Skip to content

Commit 5c2e11b

Browse files
authored
Merge pull request #134 from Virtual-Protocol/feat/yang-update-acp-client-and-docs
feat: add show hidden offerings and revamp helper functions
2 parents 2f2312a + 534803e commit 5c2e11b

67 files changed

Lines changed: 567 additions & 3017 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The Agent Commerce Protocol (ACP) Python SDK is a modular, agentic-framework-agn
3232

3333
---
3434

35-
<img src="docs/imgs/acp-banner.jpeg" width="100%" height="auto">
35+
<img src="https://github.com/Virtual-Protocol/acp-python/raw/main/docs/imgs/acp-banner.jpeg" width="100%" height="auto" alt="acp-banner">
3636

3737
---
3838

@@ -98,7 +98,7 @@ from virtuals_acp.env import EnvSettings
9898
```python
9999
env = EnvSettings()
100100

101-
acp = VirtualsACP(
101+
acp_client = VirtualsACP(
102102
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY,
103103
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS,
104104
config=BASE_SEPOLIA_CONFIG,
@@ -134,23 +134,25 @@ Available Manual Sort Metrics (via `ACPAgentSort`)
134134
```python
135135
# Matching (and sorting) via embedding similarity, followed by sorting using agent metrics
136136
relevant_agents = acp.browse_agents(
137-
keyword="<your_search_term>",
138-
cluster="<your_cluster_name>", # usually not needed
137+
keyword="<your-search-term>",
138+
cluster="<your-cluster-name>", # usually not needed
139139
sortBy=[
140140
ACPAgentSort.SUCCESSFUL_JOB_COUNT
141141
],
142142
top_k=5,
143143
graduation_status=ACPGraduationStatus.ALL,
144-
online_status=ACPOnlineStatus.ALL
144+
online_status=ACPOnlineStatus.ALL,
145+
show_hidden_offerings=True,
145146
)
146147

147148
# OR only matching (and sorting) via embedding similarity
148149
relevant_agents = acp.browse_agents(
149-
keyword="<your_search_term>",
150-
cluster="<your_cluster_name>", # usually not needed
150+
keyword="<your-search-term>",
151+
cluster="<your-cluster-name>", # usually not needed
151152
top_k=5,
152153
graduation_status=ACPGraduationStatus.ALL,
153-
online_status=ACPOnlineStatus.ALL
154+
online_status=ACPOnlineStatus.ALL,
155+
show_hidden_offerings=True,
154156
)
155157
```
156158

@@ -168,8 +170,8 @@ job_id = acp.initiate_job(
168170
)
169171

170172
# Option 2: Using a chosen job offering (e.g., from agent.browseAgents() from Agent Discovery Section)
171-
# Pick one of the agents based on your criteria (in this example we just pick the second one)
172-
chosen_agent = relevant_agents[1]
173+
# Pick one of the agents based on your criteria (in this example we just pick the first one)
174+
chosen_agent = relevant_agents[0]
173175
# Pick one of the service offerings based on your criteria (in this example we just pick the first one)
174176
chosen_agent_offering = chosen_agent.offerings[0]
175177
job_id = chosen_agent_offering.initiate_job(
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
WHITELISTED_WALLET_PRIVATE_KEY=<whitelisted-wallet-private-key>
2-
BUYER_AGENT_WALLET_ADDRESS=<buyer-agent-wallet-address>
1+
WHITELISTED_WALLET_PRIVATE_KEY=<0x-whitelisted-wallet-private-key>
2+
3+
BUYER_AGENT_WALLET_ADDRESS=<buyer-wallet-address>
34
BUYER_ENTITY_ID=<buyer-entity-id>
45

5-
SELLER_AGENT_WALLET_ADDRESS=<seller-agent-wallet-address>
6+
SELLER_AGENT_WALLET_ADDRESS=<seller-wallet-address>
67
SELLER_ENTITY_ID=<seller-entity-id>
78

8-
EVALUATOR_AGENT_WALLET_ADDRESS=<evaluator-agent-wallet-address>
9+
EVALUATOR_AGENT_WALLET_ADDRESS=<evaluator-wallet-address>
910
EVALUATOR_ENTITY_ID=<evaluator-entity-id>

examples/acp_base/external_evaluation/README.md

Lines changed: 13 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
- [Buyer](#buyer)
1515
- [Seller](#seller)
1616
- [Evaluator](#evaluator)
17-
- [Job Queue Logic](#job-queue-logic)
1817
- [How to Run](#how-to-run)
1918
- [Optional Flow: Job Offerings](#optional-flow-job-offerings)
2019
- [🚀 Job Offering Setup in ACP Visualiser](#job-offering-setup-in-acp-visualiser)
@@ -61,7 +60,7 @@ This example simulates a full job lifecycle between a buyer, seller, and evaluat
6160
- Keeps running to listen for new tasks.
6261

6362
### Evaluator
64-
- **File:** `eval.py`
63+
- **File:** `evaluator.py`
6564
- **Key Steps:**
6665
- Loads environment variables and initializes the ACP client.
6766
- Listens for jobs that require evaluation.
@@ -70,68 +69,14 @@ This example simulates a full job lifecycle between a buyer, seller, and evaluat
7069

7170
---
7271

73-
## Job Queue Logic
74-
75-
To efficiently handle multiple incoming jobs and avoid race conditions, the example scripts implement a thread-safe job queue:
76-
77-
- **Threaded Worker:** A background thread continuously processes jobs from the queue.
78-
- **Thread Safety:** A lock ensures that jobs are safely added and removed from the queue, even if multiple jobs arrive at the same time.
79-
- **Event-Driven:** When a new job arrives (via the `on_new_task` callback), it is appended to the queue and the worker is notified.
80-
81-
**How it works:**
82-
83-
```python
84-
from collections import deque
85-
86-
job_queue = deque()
87-
job_queue_lock = threading.Lock()
88-
initiate_job_lock = threading.Lock()
89-
job_event = threading.Event()
90-
91-
def safe_append_job(job):
92-
with job_queue_lock:
93-
job_queue.append(job)
94-
95-
def safe_pop_job():
96-
with job_queue_lock:
97-
if job_queue:
98-
return job_queue.popleft()
99-
return None
100-
101-
def job_worker():
102-
while True:
103-
job_event.wait()
104-
while True:
105-
job = safe_pop_job()
106-
if not job:
107-
break
108-
process_job(job)
109-
with job_queue_lock:
110-
if not job_queue:
111-
job_event.clear()
112-
113-
def on_new_task(job):
114-
safe_append_job(job)
115-
job_event.set()
116-
```
117-
118-
- This logic is used in both `buyer.py` and `seller.py` (and `eval.py` if present).
119-
- The queue ensures jobs are processed in order and safely, even under high concurrency.
120-
121-
**Why use a job queue?**
122-
- Prevents lost or overlapping jobs when multiple arrive at once.
123-
- Makes the agent robust for real-world, concurrent job handling.
124-
125-
---
126-
12772
## How to Run
12873

12974
1. **Set up your environment variables** (see the main README for details).
13075
2. **Register your agents** (buyer, seller, evaluator) in the [Service Registry](https://app.virtuals.io/acp).
13176
3. **Run each script in a separate terminal:**
13277
- `python buyer.py`
13378
- `python seller.py`
134-
- `python eval.py`
79+
- `python evaluator.py`
13580
4. **Follow the logs** to observe the full job lifecycle and external evaluation process.
13681

13782
---
@@ -151,19 +96,21 @@ You can customize agent discovery and job selection using:
15196

15297
```python
15398
# Browse available agents based on a keyword and cluster name
154-
agents = acp.browse_agents(
155-
keyword="<your_filter_agent_keyword>",
156-
cluster="<your_cluster_name>",
157-
sort=["<sort-list>"],
158-
top_k= "<top_k>",
99+
relevant_agents = acp.browse_agents(
100+
keyword="<your-filter-agent-keyword>",
101+
sort_by=[ACPAgentSort.SUCCESSFUL_JOB_COUNT],
102+
top_k=5,
159103
graduation_status=ACPGraduationStatus.ALL,
160-
online_status=ACPOnlineStatus.ALL
104+
online_status=ACPOnlineStatus.ALL,
105+
show_hidden_offerings=True,
161106
)
107+
print(f"Relevant agents: {relevant_agents}")
162108

163-
# Agents[1] assumes you have at least 2 matching agents; use with care
109+
# Pick the first agent
110+
chosen_agent = relevant_agents[0]
164111

165-
# Here, we're just picking the second agent (agents[1]) and its first offering for demo purposes
166-
job_offering = agents[1].offerings[0]
112+
# Pick the first job offering
113+
chosen_job_offering = chosen_agent.job_offerings[0]
167114
```
168115

169116
This allows you to filter agents and select specific job offerings before initiating a job. See the [main README](../../../README.md#agent-discovery) for more details on agent browsing.

examples/acp_base/external_evaluation/buyer.py

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import logging
12
import threading
23
from datetime import datetime, timedelta
34
from typing import Optional
45

56
from dotenv import load_dotenv
6-
7-
from virtuals_acp import ACPMemo
7+
from virtuals_acp.memo import ACPMemo
88
from virtuals_acp.client import VirtualsACP
99
from virtuals_acp.env import EnvSettings
1010
from virtuals_acp.job import ACPJob
@@ -14,78 +14,85 @@
1414
ACPGraduationStatus,
1515
ACPOnlineStatus,
1616
)
17+
from virtuals_acp.configs.configs import BASE_MAINNET_ACP_X402_CONFIG_V2
18+
from virtuals_acp.contract_clients.contract_client_v2 import ACPContractClientV2
1719

18-
load_dotenv(override=True)
20+
# Configure logging
21+
logging.basicConfig(
22+
level=logging.INFO,
23+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
24+
)
25+
logger = logging.getLogger("BuyerAgent")
1926

27+
load_dotenv(override=True)
2028

2129
def buyer():
2230
env = EnvSettings()
2331

2432
def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None):
25-
print(f"[on_new_task] Received job {job.id} (phase: {job.phase})")
33+
logger.info(f"[on_new_task] Received job {job.id} (phase: {job.phase})")
2634
if (
2735
job.phase == ACPJobPhase.NEGOTIATION
2836
and memo_to_sign is not None
2937
and memo_to_sign.next_phase == ACPJobPhase.TRANSACTION
3038
):
31-
print("Paying job", job.id)
32-
job.pay(job.price)
39+
logger.info(f"Paying job {job.id}")
40+
job.pay_and_accept_requirement()
41+
logger.info(f"Job {job.id} paid")
42+
elif (
43+
job.phase == ACPJobPhase.TRANSACTION
44+
and memo_to_sign is not None
45+
and memo_to_sign.next_phase == ACPJobPhase.REJECTED
46+
):
47+
logger.info(f"Signing job rejection memo {job}")
48+
memo_to_sign.sign(True, "accepts job rejection")
49+
logger.info(f"Job {job.id} rejection memo signed")
3350
elif job.phase == ACPJobPhase.COMPLETED:
34-
print("Job completed", job)
51+
logger.info(f"Job {job.id} completed")
3552
elif job.phase == ACPJobPhase.REJECTED:
36-
print("Job rejected", job)
37-
38-
def on_evaluate(job: ACPJob):
39-
print(f"Evaluation function called for job {job.id}")
40-
job.evaluate(True)
41-
42-
if env.WHITELISTED_WALLET_PRIVATE_KEY is None:
43-
raise Exception("WHITELISTED_WALLET_PRIVATE_KEY is not set")
44-
if env.BUYER_ENTITY_ID is None:
45-
raise Exception("BUYER_ENTITY_ID is not set")
46-
if env.BUYER_AGENT_WALLET_ADDRESS is None:
47-
raise Exception("BUYER_AGENT_WALLET_ADDRESS is not set")
48-
49-
acp = VirtualsACP(
50-
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY,
51-
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS,
52-
on_new_task=on_new_task,
53-
on_evaluate=on_evaluate,
54-
entity_id=env.BUYER_ENTITY_ID,
53+
logger.info(f"Job {job.id} rejected")
54+
55+
acp_client = VirtualsACP(
56+
acp_contract_clients=ACPContractClientV2(
57+
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY,
58+
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS,
59+
entity_id=env.BUYER_ENTITY_ID,
60+
config=BASE_MAINNET_ACP_X402_CONFIG_V2, # route to x402 for payment, undefined defaulted back to direct transfer
61+
),
62+
on_new_task=on_new_task
5563
)
5664

57-
# Browse available agents based on a keyword and cluster name
58-
relevant_agents = acp.browse_agents(
59-
keyword="<your_filter_agent_keyword>",
60-
cluster="<your_cluster_name>",
61-
sort_by=[
62-
ACPAgentSort.SUCCESSFUL_JOB_COUNT,
63-
],
65+
# Browse available agents
66+
relevant_agents = acp_client.browse_agents(
67+
keyword="<your-filter-agent-keyword>",
68+
sort_by=[ACPAgentSort.SUCCESSFUL_JOB_COUNT],
6469
top_k=5,
6570
graduation_status=ACPGraduationStatus.ALL,
6671
online_status=ACPOnlineStatus.ALL,
72+
show_hidden_offerings=True,
6773
)
68-
print(f"Relevant agents: {relevant_agents}")
74+
logger.info(f"Relevant agents: {relevant_agents}")
6975

70-
# Pick one of the agents based on your criteria (in this example we just pick the first one)
76+
# Pick the first agent
7177
chosen_agent = relevant_agents[0]
7278

73-
# Pick one of the service offerings based on your criteria (in this example we just pick the first one)
74-
chosen_job_offering = chosen_agent.offerings[0]
79+
# Pick the first job offering
80+
chosen_job_offering = chosen_agent.job_offerings[0]
7581

82+
# Initiate job with plain string requirement
7683
job_id = chosen_job_offering.initiate_job(
77-
# <your_schema_field> can be found in your ACP Visualiser's "Edit Service" pop-up.
78-
# Reference: (./images/specify_requirement_toggle_switch.png)
7984
service_requirement={
80-
"<your_schema_field>": "Help me to generate a flower meme."
85+
"<your-schema-key-1>": "<your-schema-value-1>",
86+
"<your-schema-key-2>": "<your-schema-value-2>",
8187
},
82-
evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS,
83-
expired_at=datetime.now() + timedelta(days=1),
88+
evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, # evaluator address
89+
expired_at=datetime.now() + timedelta(minutes=3.1) # job expiry duration, minimum 3 minutes
8490
)
8591

86-
print(f"Job {job_id} initiated")
87-
print("Listening for next steps...")
88-
# Keep the script running to listen for next steps
92+
logger.info(f"Job {job_id} initiated")
93+
logger.info("Listening for next steps...")
94+
95+
# Keep script alive
8996
threading.Event().wait()
9097

9198

0 commit comments

Comments
 (0)