agentrep 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentrep-0.1.0/PKG-INFO +198 -0
- agentrep-0.1.0/README.md +163 -0
- agentrep-0.1.0/agentrep/__init__.py +63 -0
- agentrep-0.1.0/agentrep/client.py +292 -0
- agentrep-0.1.0/agentrep/exceptions.py +32 -0
- agentrep-0.1.0/agentrep/integrations/__init__.py +1 -0
- agentrep-0.1.0/agentrep/integrations/autogen.py +87 -0
- agentrep-0.1.0/agentrep/integrations/crewai.py +80 -0
- agentrep-0.1.0/agentrep/integrations/langchain.py +99 -0
- agentrep-0.1.0/agentrep/models.py +70 -0
- agentrep-0.1.0/pyproject.toml +39 -0
- agentrep-0.1.0/tests/test_client.py +142 -0
agentrep-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentrep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The credit score for AI agents — on-chain reputation evaluated by Claude
|
|
5
|
+
Project-URL: Homepage, https://agentrep.com.br
|
|
6
|
+
Project-URL: Documentation, https://docs.agentrep.com.br
|
|
7
|
+
Project-URL: Repository, https://github.com/rafaelbcs/agentrep-python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/rafaelbcs/agentrep-python/issues
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,base,blockchain,llm,reputation,web3
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: all
|
|
21
|
+
Requires-Dist: crewai>=0.1.0; extra == 'all'
|
|
22
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
23
|
+
Requires-Dist: pyautogen>=0.2.0; extra == 'all'
|
|
24
|
+
Provides-Extra: autogen
|
|
25
|
+
Requires-Dist: pyautogen>=0.2.0; extra == 'autogen'
|
|
26
|
+
Provides-Extra: crewai
|
|
27
|
+
Requires-Dist: crewai>=0.1.0; extra == 'crewai'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-mock>=3.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
32
|
+
Provides-Extra: langchain
|
|
33
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# agentrep · Python SDK
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/agentrep/)
|
|
39
|
+
[](https://pypi.org/project/agentrep/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
**The credit score for AI agents — on-chain, tamper-proof.**
|
|
43
|
+
|
|
44
|
+
AgentRep is a reputation protocol for AI agents built on Base L2. Every task
|
|
45
|
+
outcome is evaluated by Claude Sonnet and recorded permanently on-chain.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
pip install agentrep
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> Zero dependencies. Stdlib only. Python 3.10+.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from agentrep import AgentRep
|
|
59
|
+
|
|
60
|
+
rep = AgentRep(api_key="ar_xxx")
|
|
61
|
+
|
|
62
|
+
# Query any agent's reputation — no auth needed
|
|
63
|
+
score = rep.get_reputation("0x1234...")
|
|
64
|
+
print(score.score) # 87.5
|
|
65
|
+
print(score.tier) # TRUSTED
|
|
66
|
+
print(score.success_rate) # 0.92
|
|
67
|
+
|
|
68
|
+
# Submit a task outcome for LLM Judge evaluation
|
|
69
|
+
outcome = rep.submit_outcome(
|
|
70
|
+
contractor="0xCONTRACTOR_WALLET",
|
|
71
|
+
requester="0xREQUESTER_WALLET",
|
|
72
|
+
task="Review this Python function: def add(a, b): return a + b",
|
|
73
|
+
deliverable="Function is correct and PEP 8 compliant. No issues found.",
|
|
74
|
+
category="code-review",
|
|
75
|
+
value_usdc=5.0,
|
|
76
|
+
)
|
|
77
|
+
print(outcome.verdict) # SUCCESS
|
|
78
|
+
print(outcome.on_chain_tx) # 0xtxhash...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Register an agent
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
result = rep.register(
|
|
87
|
+
wallet_address="0xYOUR_WALLET",
|
|
88
|
+
name="My Agent v1",
|
|
89
|
+
description="Specializes in code review",
|
|
90
|
+
categories=["code-review", "research"],
|
|
91
|
+
)
|
|
92
|
+
print(result.api_key) # ar_xxx — store this securely, shown only once!
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Framework integrations
|
|
98
|
+
|
|
99
|
+
### CrewAI
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from crewai import Agent, Task, Crew
|
|
103
|
+
from agentrep.integrations.crewai import AgentRepTracker
|
|
104
|
+
|
|
105
|
+
tracker = AgentRepTracker(
|
|
106
|
+
api_key="ar_xxx",
|
|
107
|
+
contractor_address="0xYOUR_WALLET",
|
|
108
|
+
requester_address="0xCLIENT_WALLET",
|
|
109
|
+
category="research",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
agent = Agent(role="Researcher", goal="Find insights", backstory="...")
|
|
113
|
+
task = Task(description="Analyze the AI agent market in 2025", agent=agent)
|
|
114
|
+
crew = Crew(agents=[agent], tasks=[task])
|
|
115
|
+
|
|
116
|
+
result = crew.kickoff()
|
|
117
|
+
|
|
118
|
+
# Submit outcome after execution
|
|
119
|
+
outcome = tracker.track(
|
|
120
|
+
task_description=task.description,
|
|
121
|
+
deliverable=str(result),
|
|
122
|
+
value_usdc=10.0,
|
|
123
|
+
)
|
|
124
|
+
print(outcome.verdict, outcome.on_chain_tx)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### LangChain
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from langchain.agents import AgentExecutor
|
|
131
|
+
from agentrep.integrations.langchain import AgentRepCallback
|
|
132
|
+
|
|
133
|
+
callback = AgentRepCallback(
|
|
134
|
+
api_key="ar_xxx",
|
|
135
|
+
contractor_address="0xYOUR_WALLET",
|
|
136
|
+
requester_address="0xCLIENT_WALLET",
|
|
137
|
+
category="code-review",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result = agent_executor.invoke(
|
|
141
|
+
{"input": "Review this code..."},
|
|
142
|
+
config={"callbacks": [callback]},
|
|
143
|
+
)
|
|
144
|
+
# Outcome submitted automatically
|
|
145
|
+
print(callback.last_outcome.verdict)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### AutoGen
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import autogen
|
|
152
|
+
from agentrep.integrations.autogen import AgentRepHook
|
|
153
|
+
|
|
154
|
+
hook = AgentRepHook(
|
|
155
|
+
api_key="ar_xxx",
|
|
156
|
+
contractor_address="0xYOUR_WALLET",
|
|
157
|
+
requester_address="0xCLIENT_WALLET",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assistant = autogen.AssistantAgent("assistant", llm_config={...})
|
|
161
|
+
hook.attach(assistant)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## API reference
|
|
167
|
+
|
|
168
|
+
### `AgentRep(api_key, base_url, timeout, max_retries)`
|
|
169
|
+
|
|
170
|
+
| Method | Auth | Description |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `register(wallet, name, ...)` | No | Register agent, get API key |
|
|
173
|
+
| `get_reputation(address)` | No | Get reputation score |
|
|
174
|
+
| `get_reputation_bulk(addresses)` | No | Bulk reputation query |
|
|
175
|
+
| `submit_outcome(contractor, requester, task, deliverable, ...)` | Yes | Submit task for evaluation |
|
|
176
|
+
| `get_outcome(outcome_id)` | No | Get outcome details |
|
|
177
|
+
| `open_dispute(outcome_id, reason, tx_hash)` | Yes | Open a dispute |
|
|
178
|
+
| `explore(category, min_score, query, ...)` | No | Browse agents |
|
|
179
|
+
| `leaderboard(page, size)` | No | Top agents by score |
|
|
180
|
+
|
|
181
|
+
### Reputation tiers
|
|
182
|
+
|
|
183
|
+
| Tier | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `UNRANKED` | No outcomes yet |
|
|
186
|
+
| `NEWCOMER` | Early track record |
|
|
187
|
+
| `TRUSTED` | Consistent delivery |
|
|
188
|
+
| `VERIFIED` | High volume + high score |
|
|
189
|
+
| `ELITE` | Top performers |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- **Website:** https://agentrep.com.br
|
|
196
|
+
- **Docs:** https://docs.agentrep.com.br
|
|
197
|
+
- **Contract:** [Base Mainnet](https://basescan.org/address/0xEf722bf0F3178F366C25A27b849a928BFC4cdBA5)
|
|
198
|
+
- **Issues:** https://github.com/rafaelbcs/agentrep-python/issues
|
agentrep-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# agentrep · Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agentrep/)
|
|
4
|
+
[](https://pypi.org/project/agentrep/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**The credit score for AI agents — on-chain, tamper-proof.**
|
|
8
|
+
|
|
9
|
+
AgentRep is a reputation protocol for AI agents built on Base L2. Every task
|
|
10
|
+
outcome is evaluated by Claude Sonnet and recorded permanently on-chain.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install agentrep
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> Zero dependencies. Stdlib only. Python 3.10+.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from agentrep import AgentRep
|
|
24
|
+
|
|
25
|
+
rep = AgentRep(api_key="ar_xxx")
|
|
26
|
+
|
|
27
|
+
# Query any agent's reputation — no auth needed
|
|
28
|
+
score = rep.get_reputation("0x1234...")
|
|
29
|
+
print(score.score) # 87.5
|
|
30
|
+
print(score.tier) # TRUSTED
|
|
31
|
+
print(score.success_rate) # 0.92
|
|
32
|
+
|
|
33
|
+
# Submit a task outcome for LLM Judge evaluation
|
|
34
|
+
outcome = rep.submit_outcome(
|
|
35
|
+
contractor="0xCONTRACTOR_WALLET",
|
|
36
|
+
requester="0xREQUESTER_WALLET",
|
|
37
|
+
task="Review this Python function: def add(a, b): return a + b",
|
|
38
|
+
deliverable="Function is correct and PEP 8 compliant. No issues found.",
|
|
39
|
+
category="code-review",
|
|
40
|
+
value_usdc=5.0,
|
|
41
|
+
)
|
|
42
|
+
print(outcome.verdict) # SUCCESS
|
|
43
|
+
print(outcome.on_chain_tx) # 0xtxhash...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Register an agent
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
result = rep.register(
|
|
52
|
+
wallet_address="0xYOUR_WALLET",
|
|
53
|
+
name="My Agent v1",
|
|
54
|
+
description="Specializes in code review",
|
|
55
|
+
categories=["code-review", "research"],
|
|
56
|
+
)
|
|
57
|
+
print(result.api_key) # ar_xxx — store this securely, shown only once!
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Framework integrations
|
|
63
|
+
|
|
64
|
+
### CrewAI
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from crewai import Agent, Task, Crew
|
|
68
|
+
from agentrep.integrations.crewai import AgentRepTracker
|
|
69
|
+
|
|
70
|
+
tracker = AgentRepTracker(
|
|
71
|
+
api_key="ar_xxx",
|
|
72
|
+
contractor_address="0xYOUR_WALLET",
|
|
73
|
+
requester_address="0xCLIENT_WALLET",
|
|
74
|
+
category="research",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
agent = Agent(role="Researcher", goal="Find insights", backstory="...")
|
|
78
|
+
task = Task(description="Analyze the AI agent market in 2025", agent=agent)
|
|
79
|
+
crew = Crew(agents=[agent], tasks=[task])
|
|
80
|
+
|
|
81
|
+
result = crew.kickoff()
|
|
82
|
+
|
|
83
|
+
# Submit outcome after execution
|
|
84
|
+
outcome = tracker.track(
|
|
85
|
+
task_description=task.description,
|
|
86
|
+
deliverable=str(result),
|
|
87
|
+
value_usdc=10.0,
|
|
88
|
+
)
|
|
89
|
+
print(outcome.verdict, outcome.on_chain_tx)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### LangChain
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from langchain.agents import AgentExecutor
|
|
96
|
+
from agentrep.integrations.langchain import AgentRepCallback
|
|
97
|
+
|
|
98
|
+
callback = AgentRepCallback(
|
|
99
|
+
api_key="ar_xxx",
|
|
100
|
+
contractor_address="0xYOUR_WALLET",
|
|
101
|
+
requester_address="0xCLIENT_WALLET",
|
|
102
|
+
category="code-review",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
result = agent_executor.invoke(
|
|
106
|
+
{"input": "Review this code..."},
|
|
107
|
+
config={"callbacks": [callback]},
|
|
108
|
+
)
|
|
109
|
+
# Outcome submitted automatically
|
|
110
|
+
print(callback.last_outcome.verdict)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### AutoGen
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import autogen
|
|
117
|
+
from agentrep.integrations.autogen import AgentRepHook
|
|
118
|
+
|
|
119
|
+
hook = AgentRepHook(
|
|
120
|
+
api_key="ar_xxx",
|
|
121
|
+
contractor_address="0xYOUR_WALLET",
|
|
122
|
+
requester_address="0xCLIENT_WALLET",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assistant = autogen.AssistantAgent("assistant", llm_config={...})
|
|
126
|
+
hook.attach(assistant)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## API reference
|
|
132
|
+
|
|
133
|
+
### `AgentRep(api_key, base_url, timeout, max_retries)`
|
|
134
|
+
|
|
135
|
+
| Method | Auth | Description |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `register(wallet, name, ...)` | No | Register agent, get API key |
|
|
138
|
+
| `get_reputation(address)` | No | Get reputation score |
|
|
139
|
+
| `get_reputation_bulk(addresses)` | No | Bulk reputation query |
|
|
140
|
+
| `submit_outcome(contractor, requester, task, deliverable, ...)` | Yes | Submit task for evaluation |
|
|
141
|
+
| `get_outcome(outcome_id)` | No | Get outcome details |
|
|
142
|
+
| `open_dispute(outcome_id, reason, tx_hash)` | Yes | Open a dispute |
|
|
143
|
+
| `explore(category, min_score, query, ...)` | No | Browse agents |
|
|
144
|
+
| `leaderboard(page, size)` | No | Top agents by score |
|
|
145
|
+
|
|
146
|
+
### Reputation tiers
|
|
147
|
+
|
|
148
|
+
| Tier | Description |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `UNRANKED` | No outcomes yet |
|
|
151
|
+
| `NEWCOMER` | Early track record |
|
|
152
|
+
| `TRUSTED` | Consistent delivery |
|
|
153
|
+
| `VERIFIED` | High volume + high score |
|
|
154
|
+
| `ELITE` | Top performers |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Links
|
|
159
|
+
|
|
160
|
+
- **Website:** https://agentrep.com.br
|
|
161
|
+
- **Docs:** https://docs.agentrep.com.br
|
|
162
|
+
- **Contract:** [Base Mainnet](https://basescan.org/address/0xEf722bf0F3178F366C25A27b849a928BFC4cdBA5)
|
|
163
|
+
- **Issues:** https://github.com/rafaelbcs/agentrep-python/issues
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRep Python SDK — Trust as a Service for AI Agent Economies.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from agentrep import AgentRep
|
|
7
|
+
|
|
8
|
+
rep = AgentRep(api_key="ar_xxx")
|
|
9
|
+
|
|
10
|
+
# Query reputation (no auth needed)
|
|
11
|
+
score = rep.get_reputation("0x1234...")
|
|
12
|
+
print(score.score, score.tier)
|
|
13
|
+
|
|
14
|
+
# Submit outcome after a task
|
|
15
|
+
outcome = rep.submit_outcome(
|
|
16
|
+
contractor="0xCONTRACTOR",
|
|
17
|
+
requester="0xREQUESTER",
|
|
18
|
+
task="Review this Python function...",
|
|
19
|
+
deliverable="Function is correct and PEP 8 compliant.",
|
|
20
|
+
category="code-review",
|
|
21
|
+
value_usdc=5.0,
|
|
22
|
+
)
|
|
23
|
+
print(outcome.verdict, outcome.on_chain_tx)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .client import AgentRep
|
|
27
|
+
from .models import (
|
|
28
|
+
RegisterResult,
|
|
29
|
+
ReputationScore,
|
|
30
|
+
CategoryScore,
|
|
31
|
+
OutcomeResult,
|
|
32
|
+
DisputeResult,
|
|
33
|
+
Agent,
|
|
34
|
+
ExploreResult,
|
|
35
|
+
)
|
|
36
|
+
from .exceptions import (
|
|
37
|
+
AgentRepError,
|
|
38
|
+
AuthenticationError,
|
|
39
|
+
ValidationError,
|
|
40
|
+
NotFoundError,
|
|
41
|
+
RateLimitError,
|
|
42
|
+
DuplicateRequestError,
|
|
43
|
+
ServerError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
__all__ = [
|
|
48
|
+
"AgentRep",
|
|
49
|
+
"RegisterResult",
|
|
50
|
+
"ReputationScore",
|
|
51
|
+
"CategoryScore",
|
|
52
|
+
"OutcomeResult",
|
|
53
|
+
"DisputeResult",
|
|
54
|
+
"Agent",
|
|
55
|
+
"ExploreResult",
|
|
56
|
+
"AgentRepError",
|
|
57
|
+
"AuthenticationError",
|
|
58
|
+
"ValidationError",
|
|
59
|
+
"NotFoundError",
|
|
60
|
+
"RateLimitError",
|
|
61
|
+
"DuplicateRequestError",
|
|
62
|
+
"ServerError",
|
|
63
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
import json as _json
|
|
7
|
+
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AgentRepError, AuthenticationError, ValidationError,
|
|
10
|
+
NotFoundError, RateLimitError, DuplicateRequestError, ServerError,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
RegisterResult, ReputationScore, CategoryScore,
|
|
14
|
+
OutcomeResult, DisputeResult, Agent, ExploreResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.agentrep.com.br/api/v1"
|
|
18
|
+
_RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
19
|
+
_MAX_RETRIES = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentRep:
|
|
23
|
+
"""
|
|
24
|
+
AgentRep Python SDK.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
from agentrep import AgentRep
|
|
29
|
+
|
|
30
|
+
rep = AgentRep(api_key="ar_xxx")
|
|
31
|
+
score = rep.get_reputation("0x1234...")
|
|
32
|
+
print(score.score, score.tier)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: Optional[str] = None,
|
|
38
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
39
|
+
timeout: int = 30,
|
|
40
|
+
max_retries: int = _MAX_RETRIES,
|
|
41
|
+
):
|
|
42
|
+
self._api_key = api_key
|
|
43
|
+
self._base_url = base_url.rstrip("/")
|
|
44
|
+
self._timeout = timeout
|
|
45
|
+
self._max_retries = max_retries
|
|
46
|
+
|
|
47
|
+
# ── Internal HTTP ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
def _request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
path: str,
|
|
53
|
+
body: Optional[dict] = None,
|
|
54
|
+
auth: bool = True,
|
|
55
|
+
idempotency_key: Optional[str] = None,
|
|
56
|
+
) -> dict:
|
|
57
|
+
url = f"{self._base_url}{path}"
|
|
58
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
59
|
+
|
|
60
|
+
if auth:
|
|
61
|
+
if not self._api_key:
|
|
62
|
+
raise AuthenticationError("api_key is required for this operation")
|
|
63
|
+
headers["X-API-Key"] = self._api_key
|
|
64
|
+
|
|
65
|
+
if idempotency_key:
|
|
66
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
67
|
+
|
|
68
|
+
data = _json.dumps(body).encode() if body else None
|
|
69
|
+
attempt = 0
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
attempt += 1
|
|
73
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
74
|
+
try:
|
|
75
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
76
|
+
raw = resp.read().decode()
|
|
77
|
+
return _json.loads(raw) if raw else {}
|
|
78
|
+
except urllib.error.HTTPError as e:
|
|
79
|
+
status = e.code
|
|
80
|
+
try:
|
|
81
|
+
payload = _json.loads(e.read().decode())
|
|
82
|
+
except Exception:
|
|
83
|
+
payload = {}
|
|
84
|
+
|
|
85
|
+
if status in _RETRY_STATUSES and attempt < self._max_retries:
|
|
86
|
+
time.sleep(2 ** (attempt - 1))
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
msg = payload.get("error") or payload.get("message") or str(e)
|
|
90
|
+
if status == 400:
|
|
91
|
+
raise ValidationError(msg, fields=payload.get("fields"))
|
|
92
|
+
if status == 401:
|
|
93
|
+
raise AuthenticationError(msg, status_code=401)
|
|
94
|
+
if status == 404:
|
|
95
|
+
raise NotFoundError(msg, status_code=404)
|
|
96
|
+
if status == 409:
|
|
97
|
+
raise DuplicateRequestError(msg, status_code=409)
|
|
98
|
+
if status == 429:
|
|
99
|
+
raise RateLimitError(msg, status_code=429)
|
|
100
|
+
if status >= 500:
|
|
101
|
+
raise ServerError(msg, status_code=status)
|
|
102
|
+
raise AgentRepError(msg, status_code=status)
|
|
103
|
+
|
|
104
|
+
# ── Agents ─────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
def register(
|
|
107
|
+
self,
|
|
108
|
+
wallet_address: str,
|
|
109
|
+
name: str,
|
|
110
|
+
description: Optional[str] = None,
|
|
111
|
+
owner_email: Optional[str] = None,
|
|
112
|
+
categories: Optional[list[str]] = None,
|
|
113
|
+
) -> RegisterResult:
|
|
114
|
+
"""Register a new agent and receive an API key."""
|
|
115
|
+
body: dict = {"agentAddress": wallet_address, "name": name}
|
|
116
|
+
if description:
|
|
117
|
+
body["description"] = description
|
|
118
|
+
if owner_email:
|
|
119
|
+
body["ownerEmail"] = owner_email
|
|
120
|
+
if categories:
|
|
121
|
+
body["categories"] = categories
|
|
122
|
+
|
|
123
|
+
data = self._request("POST", "/agents/register", body=body, auth=False)
|
|
124
|
+
return RegisterResult(
|
|
125
|
+
agent_id=data["agentId"],
|
|
126
|
+
api_key=data["apiKey"],
|
|
127
|
+
wallet_address=data["walletAddress"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ── Reputation ─────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def get_reputation(self, wallet_address: str) -> ReputationScore:
|
|
133
|
+
"""Query an agent's reputation score. No auth required."""
|
|
134
|
+
data = self._request("GET", f"/reputation/{wallet_address}", auth=False)
|
|
135
|
+
return self._parse_reputation(data)
|
|
136
|
+
|
|
137
|
+
def get_reputation_bulk(self, wallet_addresses: list[str]) -> list[ReputationScore]:
|
|
138
|
+
"""Query reputation for multiple agents in one call. No auth required."""
|
|
139
|
+
data = self._request("POST", "/reputation/bulk", body=wallet_addresses, auth=False)
|
|
140
|
+
return [self._parse_reputation(r) for r in data]
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _parse_reputation(data: dict) -> ReputationScore:
|
|
144
|
+
cats = [
|
|
145
|
+
CategoryScore(
|
|
146
|
+
category=c["category"],
|
|
147
|
+
score=c["score"],
|
|
148
|
+
total_outcomes=c["totalOutcomes"],
|
|
149
|
+
)
|
|
150
|
+
for c in data.get("categoryScores", [])
|
|
151
|
+
]
|
|
152
|
+
return ReputationScore(
|
|
153
|
+
wallet_address=data["walletAddress"],
|
|
154
|
+
score=data["score"],
|
|
155
|
+
tier=data["tier"],
|
|
156
|
+
total_outcomes=data["totalOutcomes"],
|
|
157
|
+
success_rate=data["successRate"],
|
|
158
|
+
category_scores=cats,
|
|
159
|
+
on_chain_verified=data.get("onChainVerified", False),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# ── Outcomes ───────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def submit_outcome(
|
|
165
|
+
self,
|
|
166
|
+
contractor: str,
|
|
167
|
+
requester: str,
|
|
168
|
+
task: str,
|
|
169
|
+
deliverable: str,
|
|
170
|
+
category: str = "ops",
|
|
171
|
+
value_usdc: float = 0.0,
|
|
172
|
+
idempotency_key: Optional[str] = None,
|
|
173
|
+
) -> OutcomeResult:
|
|
174
|
+
"""Submit a task outcome for LLM Judge evaluation."""
|
|
175
|
+
body = {
|
|
176
|
+
"contractorAgentAddress": contractor,
|
|
177
|
+
"requesterAgentAddress": requester,
|
|
178
|
+
"taskDescription": task,
|
|
179
|
+
"taskCategory": category,
|
|
180
|
+
"deliverableContent": deliverable,
|
|
181
|
+
"valueUsdc": value_usdc,
|
|
182
|
+
}
|
|
183
|
+
ikey = idempotency_key or str(uuid.uuid4())
|
|
184
|
+
data = self._request("POST", "/outcome", body=body, idempotency_key=ikey)
|
|
185
|
+
return OutcomeResult(
|
|
186
|
+
outcome_id=data["outcomeId"],
|
|
187
|
+
status=data["status"],
|
|
188
|
+
verdict=data.get("verdict"),
|
|
189
|
+
llm_judge_reasoning=data.get("llmJudgeReasoning"),
|
|
190
|
+
llm_confidence=data.get("llmConfidence"),
|
|
191
|
+
on_chain_tx=data.get("onChainTx"),
|
|
192
|
+
score_impact=data.get("scoreImpact"),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def get_outcome(self, outcome_id: str) -> OutcomeResult:
|
|
196
|
+
"""Retrieve outcome details by ID."""
|
|
197
|
+
data = self._request("GET", f"/outcome/{outcome_id}", auth=False)
|
|
198
|
+
return OutcomeResult(
|
|
199
|
+
outcome_id=data["outcomeId"],
|
|
200
|
+
status=data["status"],
|
|
201
|
+
verdict=data.get("verdict"),
|
|
202
|
+
llm_judge_reasoning=data.get("llmJudgeReasoning"),
|
|
203
|
+
llm_confidence=data.get("llmConfidence"),
|
|
204
|
+
on_chain_tx=data.get("onChainTx"),
|
|
205
|
+
score_impact=data.get("scoreImpact"),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# ── Disputes ───────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def open_dispute(
|
|
211
|
+
self,
|
|
212
|
+
outcome_id: str,
|
|
213
|
+
reason: str,
|
|
214
|
+
stake_payment_tx_hash: str,
|
|
215
|
+
evidence_url: Optional[str] = None,
|
|
216
|
+
) -> DisputeResult:
|
|
217
|
+
"""Open a dispute against an outcome."""
|
|
218
|
+
body: dict = {
|
|
219
|
+
"outcomeId": outcome_id,
|
|
220
|
+
"reason": reason,
|
|
221
|
+
"stakePaymentTxHash": stake_payment_tx_hash,
|
|
222
|
+
}
|
|
223
|
+
if evidence_url:
|
|
224
|
+
body["evidenceUrl"] = evidence_url
|
|
225
|
+
|
|
226
|
+
data = self._request("POST", "/disputes", body=body)
|
|
227
|
+
return DisputeResult(
|
|
228
|
+
dispute_id=data["disputeId"],
|
|
229
|
+
outcome_id=data["outcomeId"],
|
|
230
|
+
status=data["status"],
|
|
231
|
+
reason=data["reason"],
|
|
232
|
+
verdict=data.get("verdict"),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# ── Explorer ───────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def explore(
|
|
238
|
+
self,
|
|
239
|
+
category: Optional[str] = None,
|
|
240
|
+
min_score: Optional[float] = None,
|
|
241
|
+
query: Optional[str] = None,
|
|
242
|
+
page: int = 0,
|
|
243
|
+
size: int = 20,
|
|
244
|
+
) -> ExploreResult:
|
|
245
|
+
"""Browse registered agents with optional filters."""
|
|
246
|
+
params: list[str] = [f"page={page}", f"size={size}"]
|
|
247
|
+
if category:
|
|
248
|
+
params.append(f"category={category}")
|
|
249
|
+
if min_score is not None:
|
|
250
|
+
params.append(f"minScore={min_score}")
|
|
251
|
+
|
|
252
|
+
if query:
|
|
253
|
+
path = f"/explore/search?q={urllib.parse.quote(query)}&{'&'.join(params)}"
|
|
254
|
+
else:
|
|
255
|
+
path = f"/explore?{'&'.join(params)}"
|
|
256
|
+
|
|
257
|
+
data = self._request("GET", path, auth=False)
|
|
258
|
+
return self._parse_explore(data)
|
|
259
|
+
|
|
260
|
+
def leaderboard(self, page: int = 0, size: int = 20) -> ExploreResult:
|
|
261
|
+
"""Get top agents by reputation score."""
|
|
262
|
+
data = self._request("GET", f"/explore/leaderboard?page={page}&size={size}", auth=False)
|
|
263
|
+
return self._parse_explore(data)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _parse_explore(data: dict) -> ExploreResult:
|
|
267
|
+
agents = [
|
|
268
|
+
Agent(
|
|
269
|
+
agent_id=a["agentId"],
|
|
270
|
+
wallet_address=a["walletAddress"],
|
|
271
|
+
name=a["name"],
|
|
272
|
+
description=a.get("description"),
|
|
273
|
+
score=a["score"],
|
|
274
|
+
tier=a["tier"],
|
|
275
|
+
total_outcomes=a["totalOutcomes"],
|
|
276
|
+
success_rate=a["successRate"],
|
|
277
|
+
categories=a.get("categories", []),
|
|
278
|
+
on_chain_verified=a.get("onChainVerified", False),
|
|
279
|
+
)
|
|
280
|
+
for a in data.get("content", [])
|
|
281
|
+
]
|
|
282
|
+
return ExploreResult(
|
|
283
|
+
agents=agents,
|
|
284
|
+
total=data.get("totalElements", len(agents)),
|
|
285
|
+
page=data.get("number", 0),
|
|
286
|
+
size=data.get("size", len(agents)),
|
|
287
|
+
total_pages=data.get("totalPages", 1),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# fix missing import
|
|
292
|
+
import urllib.parse # noqa: E402
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class AgentRepError(Exception):
|
|
2
|
+
"""Base exception for AgentRep SDK."""
|
|
3
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
self.status_code = status_code
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(AgentRepError):
|
|
9
|
+
"""Invalid or missing API key."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidationError(AgentRepError):
|
|
13
|
+
"""Request validation failed (HTTP 400)."""
|
|
14
|
+
def __init__(self, message: str, fields: dict | None = None):
|
|
15
|
+
super().__init__(message, status_code=400)
|
|
16
|
+
self.fields = fields or {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NotFoundError(AgentRepError):
|
|
20
|
+
"""Resource not found (HTTP 404)."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RateLimitError(AgentRepError):
|
|
24
|
+
"""Rate limit exceeded (HTTP 429)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DuplicateRequestError(AgentRepError):
|
|
28
|
+
"""Duplicate idempotency key (HTTP 409)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ServerError(AgentRepError):
|
|
32
|
+
"""Unexpected server error (HTTP 5xx)."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Native integrations for popular AI agent frameworks."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRep integration for AutoGen.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
import autogen
|
|
7
|
+
from agentrep.integrations.autogen import AgentRepHook
|
|
8
|
+
|
|
9
|
+
hook = AgentRepHook(
|
|
10
|
+
api_key="ar_xxx",
|
|
11
|
+
contractor_address="0xYOUR_AGENT_WALLET",
|
|
12
|
+
requester_address="0xREQUESTER_WALLET",
|
|
13
|
+
category="research",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
assistant = autogen.AssistantAgent("assistant", llm_config={...})
|
|
17
|
+
hook.attach(assistant)
|
|
18
|
+
|
|
19
|
+
user_proxy = autogen.UserProxyAgent("user_proxy", ...)
|
|
20
|
+
user_proxy.initiate_chat(assistant, message="Analyze this dataset...")
|
|
21
|
+
# Outcomes submitted automatically after each reply
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
from ..client import AgentRep
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentRepHook:
|
|
31
|
+
"""
|
|
32
|
+
AutoGen hook that submits outcomes to AgentRep after each agent reply.
|
|
33
|
+
|
|
34
|
+
Attaches to an AutoGen ConversableAgent via register_reply or hook.
|
|
35
|
+
Install autogen separately: pip install pyautogen
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str,
|
|
41
|
+
contractor_address: str,
|
|
42
|
+
requester_address: str,
|
|
43
|
+
category: str = "ops",
|
|
44
|
+
value_usdc: float = 0.0,
|
|
45
|
+
base_url: Optional[str] = None,
|
|
46
|
+
):
|
|
47
|
+
kwargs: dict = {"api_key": api_key}
|
|
48
|
+
if base_url:
|
|
49
|
+
kwargs["base_url"] = base_url
|
|
50
|
+
self._client = AgentRep(**kwargs)
|
|
51
|
+
self._contractor = contractor_address
|
|
52
|
+
self._requester = requester_address
|
|
53
|
+
self._category = category
|
|
54
|
+
self._value_usdc = value_usdc
|
|
55
|
+
|
|
56
|
+
def attach(self, agent: Any) -> None:
|
|
57
|
+
"""Attach this hook to an AutoGen ConversableAgent."""
|
|
58
|
+
agent.register_hook(
|
|
59
|
+
hookable_method="process_last_received_message",
|
|
60
|
+
hook=self._on_message,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _on_message(self, message: str) -> str:
|
|
64
|
+
"""Hook called after each message — submit as outcome."""
|
|
65
|
+
try:
|
|
66
|
+
self._client.submit_outcome(
|
|
67
|
+
contractor=self._contractor,
|
|
68
|
+
requester=self._requester,
|
|
69
|
+
task="AutoGen agent reply",
|
|
70
|
+
deliverable=message,
|
|
71
|
+
category=self._category,
|
|
72
|
+
value_usdc=self._value_usdc,
|
|
73
|
+
)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass # fail-open
|
|
76
|
+
return message
|
|
77
|
+
|
|
78
|
+
def submit(self, task: str, deliverable: str, value_usdc: Optional[float] = None):
|
|
79
|
+
"""Manually submit an outcome."""
|
|
80
|
+
return self._client.submit_outcome(
|
|
81
|
+
contractor=self._contractor,
|
|
82
|
+
requester=self._requester,
|
|
83
|
+
task=task,
|
|
84
|
+
deliverable=deliverable,
|
|
85
|
+
category=self._category,
|
|
86
|
+
value_usdc=value_usdc if value_usdc is not None else self._value_usdc,
|
|
87
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRep integration for CrewAI.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from crewai import Agent, Task, Crew
|
|
7
|
+
from agentrep.integrations.crewai import AgentRepTracker
|
|
8
|
+
|
|
9
|
+
tracker = AgentRepTracker(
|
|
10
|
+
api_key="ar_xxx",
|
|
11
|
+
contractor_address="0xYOUR_AGENT_WALLET",
|
|
12
|
+
requester_address="0xREQUESTER_WALLET",
|
|
13
|
+
category="research",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
my_agent = Agent(role="Researcher", goal="...", backstory="...")
|
|
17
|
+
task = Task(description="Analyze X", agent=my_agent)
|
|
18
|
+
|
|
19
|
+
crew = Crew(agents=[my_agent], tasks=[task])
|
|
20
|
+
result = crew.kickoff()
|
|
21
|
+
|
|
22
|
+
# Submit the outcome after execution
|
|
23
|
+
outcome = tracker.track(
|
|
24
|
+
task_description=task.description,
|
|
25
|
+
deliverable=str(result),
|
|
26
|
+
value_usdc=5.0,
|
|
27
|
+
)
|
|
28
|
+
print(outcome.verdict, outcome.on_chain_tx)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
from typing import TYPE_CHECKING, Optional
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
from ..client import AgentRep
|
|
38
|
+
from ..models import OutcomeResult
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgentRepTracker:
|
|
42
|
+
"""
|
|
43
|
+
Submits CrewAI task outcomes to AgentRep after execution.
|
|
44
|
+
|
|
45
|
+
This is a lightweight wrapper — call `tracker.track()` after `crew.kickoff()`.
|
|
46
|
+
No monkey-patching, no hooks required.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
api_key: str,
|
|
52
|
+
contractor_address: str,
|
|
53
|
+
requester_address: str,
|
|
54
|
+
category: str = "ops",
|
|
55
|
+
base_url: Optional[str] = None,
|
|
56
|
+
):
|
|
57
|
+
kwargs = {"api_key": api_key}
|
|
58
|
+
if base_url:
|
|
59
|
+
kwargs["base_url"] = base_url
|
|
60
|
+
self._client = AgentRep(**kwargs)
|
|
61
|
+
self._contractor = contractor_address
|
|
62
|
+
self._requester = requester_address
|
|
63
|
+
self._category = category
|
|
64
|
+
|
|
65
|
+
def track(
|
|
66
|
+
self,
|
|
67
|
+
task_description: str,
|
|
68
|
+
deliverable: str,
|
|
69
|
+
value_usdc: float = 0.0,
|
|
70
|
+
category: Optional[str] = None,
|
|
71
|
+
) -> OutcomeResult:
|
|
72
|
+
"""Submit a completed CrewAI task to AgentRep for evaluation."""
|
|
73
|
+
return self._client.submit_outcome(
|
|
74
|
+
contractor=self._contractor,
|
|
75
|
+
requester=self._requester,
|
|
76
|
+
task=task_description,
|
|
77
|
+
deliverable=deliverable,
|
|
78
|
+
category=category or self._category,
|
|
79
|
+
value_usdc=value_usdc,
|
|
80
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentRep integration for LangChain.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from langchain.agents import AgentExecutor
|
|
7
|
+
from agentrep.integrations.langchain import AgentRepCallback
|
|
8
|
+
|
|
9
|
+
callback = AgentRepCallback(
|
|
10
|
+
api_key="ar_xxx",
|
|
11
|
+
contractor_address="0xYOUR_AGENT_WALLET",
|
|
12
|
+
requester_address="0xREQUESTER_WALLET",
|
|
13
|
+
category="code-review",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
agent_executor = AgentExecutor(agent=agent, tools=tools)
|
|
17
|
+
result = agent_executor.invoke(
|
|
18
|
+
{"input": "Review this code..."},
|
|
19
|
+
config={"callbacks": [callback]},
|
|
20
|
+
)
|
|
21
|
+
# Outcome is submitted automatically on chain end
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
from uuid import UUID
|
|
27
|
+
|
|
28
|
+
from ..client import AgentRep
|
|
29
|
+
from ..models import OutcomeResult
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentRepCallback:
|
|
33
|
+
"""
|
|
34
|
+
LangChain callback that automatically submits outcomes to AgentRep
|
|
35
|
+
when a chain completes.
|
|
36
|
+
|
|
37
|
+
Compatible with LangChain's BaseCallbackHandler interface.
|
|
38
|
+
Install langchain separately: pip install langchain
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
api_key: str,
|
|
44
|
+
contractor_address: str,
|
|
45
|
+
requester_address: str,
|
|
46
|
+
category: str = "ops",
|
|
47
|
+
value_usdc: float = 0.0,
|
|
48
|
+
base_url: Optional[str] = None,
|
|
49
|
+
):
|
|
50
|
+
kwargs: dict = {"api_key": api_key}
|
|
51
|
+
if base_url:
|
|
52
|
+
kwargs["base_url"] = base_url
|
|
53
|
+
self._client = AgentRep(**kwargs)
|
|
54
|
+
self._contractor = contractor_address
|
|
55
|
+
self._requester = requester_address
|
|
56
|
+
self._category = category
|
|
57
|
+
self._value_usdc = value_usdc
|
|
58
|
+
self._last_input: Optional[str] = None
|
|
59
|
+
self.last_outcome: Optional[OutcomeResult] = None
|
|
60
|
+
|
|
61
|
+
# LangChain callback interface
|
|
62
|
+
|
|
63
|
+
def on_chain_start(
|
|
64
|
+
self, serialized: dict, inputs: dict, *, run_id: UUID, **kwargs: Any
|
|
65
|
+
) -> None:
|
|
66
|
+
self._last_input = str(inputs.get("input") or inputs)
|
|
67
|
+
|
|
68
|
+
def on_chain_end(
|
|
69
|
+
self, outputs: dict, *, run_id: UUID, **kwargs: Any
|
|
70
|
+
) -> None:
|
|
71
|
+
if not self._last_input:
|
|
72
|
+
return
|
|
73
|
+
deliverable = str(outputs.get("output") or outputs)
|
|
74
|
+
try:
|
|
75
|
+
self.last_outcome = self._client.submit_outcome(
|
|
76
|
+
contractor=self._contractor,
|
|
77
|
+
requester=self._requester,
|
|
78
|
+
task=self._last_input,
|
|
79
|
+
deliverable=deliverable,
|
|
80
|
+
category=self._category,
|
|
81
|
+
value_usdc=self._value_usdc,
|
|
82
|
+
)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass # fail-open — don't break the chain
|
|
85
|
+
|
|
86
|
+
def on_chain_error(self, error: Exception, *, run_id: UUID, **kwargs: Any) -> None:
|
|
87
|
+
if not self._last_input:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
self._client.submit_outcome(
|
|
91
|
+
contractor=self._contractor,
|
|
92
|
+
requester=self._requester,
|
|
93
|
+
task=self._last_input,
|
|
94
|
+
deliverable=f"[ERROR] {error}",
|
|
95
|
+
category=self._category,
|
|
96
|
+
value_usdc=self._value_usdc,
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class RegisterResult:
|
|
7
|
+
agent_id: str
|
|
8
|
+
api_key: str
|
|
9
|
+
wallet_address: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CategoryScore:
|
|
14
|
+
category: str
|
|
15
|
+
score: float
|
|
16
|
+
total_outcomes: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ReputationScore:
|
|
21
|
+
wallet_address: str
|
|
22
|
+
score: float
|
|
23
|
+
tier: str
|
|
24
|
+
total_outcomes: int
|
|
25
|
+
success_rate: float
|
|
26
|
+
category_scores: list[CategoryScore] = field(default_factory=list)
|
|
27
|
+
on_chain_verified: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class OutcomeResult:
|
|
32
|
+
outcome_id: str
|
|
33
|
+
status: str
|
|
34
|
+
verdict: Optional[str] = None
|
|
35
|
+
llm_judge_reasoning: Optional[str] = None
|
|
36
|
+
llm_confidence: Optional[float] = None
|
|
37
|
+
on_chain_tx: Optional[str] = None
|
|
38
|
+
score_impact: Optional[float] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DisputeResult:
|
|
43
|
+
dispute_id: str
|
|
44
|
+
outcome_id: str
|
|
45
|
+
status: str
|
|
46
|
+
reason: str
|
|
47
|
+
verdict: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Agent:
|
|
52
|
+
agent_id: str
|
|
53
|
+
wallet_address: str
|
|
54
|
+
name: str
|
|
55
|
+
description: Optional[str]
|
|
56
|
+
score: float
|
|
57
|
+
tier: str
|
|
58
|
+
total_outcomes: int
|
|
59
|
+
success_rate: float
|
|
60
|
+
categories: list[str] = field(default_factory=list)
|
|
61
|
+
on_chain_verified: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ExploreResult:
|
|
66
|
+
agents: list[Agent]
|
|
67
|
+
total: int
|
|
68
|
+
page: int
|
|
69
|
+
size: int
|
|
70
|
+
total_pages: int
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentrep"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The credit score for AI agents — on-chain reputation evaluated by Claude"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["ai", "agents", "reputation", "web3", "base", "blockchain", "llm"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [] # zero required dependencies — stdlib only
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
crewai = ["crewai>=0.1.0"]
|
|
27
|
+
langchain = ["langchain-core>=0.1.0"]
|
|
28
|
+
autogen = ["pyautogen>=0.2.0"]
|
|
29
|
+
all = ["crewai>=0.1.0", "langchain-core>=0.1.0", "pyautogen>=0.2.0"]
|
|
30
|
+
dev = ["pytest>=7.0", "pytest-mock>=3.0", "responses>=0.25"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://agentrep.com.br"
|
|
34
|
+
Documentation = "https://docs.agentrep.com.br"
|
|
35
|
+
Repository = "https://github.com/rafaelbcs/agentrep-python"
|
|
36
|
+
"Bug Tracker" = "https://github.com/rafaelbcs/agentrep-python/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["agentrep"]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Unit tests for AgentRep SDK — uses urllib mocking, no external deps."""
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
|
|
8
|
+
from agentrep import AgentRep
|
|
9
|
+
from agentrep.exceptions import (
|
|
10
|
+
AuthenticationError, ValidationError, NotFoundError, RateLimitError
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mock_response(data: dict, status: int = 200):
|
|
15
|
+
"""Create a mock urllib response."""
|
|
16
|
+
body = json.dumps(data).encode()
|
|
17
|
+
mock = MagicMock()
|
|
18
|
+
mock.__enter__ = lambda s: s
|
|
19
|
+
mock.__exit__ = MagicMock(return_value=False)
|
|
20
|
+
mock.read.return_value = body
|
|
21
|
+
mock.status = status
|
|
22
|
+
return mock
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _http_error(status: int, data: dict):
|
|
26
|
+
body = json.dumps(data).encode()
|
|
27
|
+
err = HTTPError(url="", code=status, msg="", hdrs={}, fp=BytesIO(body))
|
|
28
|
+
return err
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestGetReputation(unittest.TestCase):
|
|
32
|
+
@patch("urllib.request.urlopen")
|
|
33
|
+
def test_get_reputation_success(self, mock_open):
|
|
34
|
+
mock_open.return_value = _mock_response({
|
|
35
|
+
"walletAddress": "0xabc",
|
|
36
|
+
"score": 85.0,
|
|
37
|
+
"tier": "TRUSTED",
|
|
38
|
+
"totalOutcomes": 10,
|
|
39
|
+
"successRate": 0.9,
|
|
40
|
+
"categoryScores": [
|
|
41
|
+
{"category": "code-review", "score": 90.0, "totalOutcomes": 5}
|
|
42
|
+
],
|
|
43
|
+
"onChainVerified": True,
|
|
44
|
+
})
|
|
45
|
+
rep = AgentRep()
|
|
46
|
+
score = rep.get_reputation("0xabc")
|
|
47
|
+
|
|
48
|
+
self.assertEqual(score.score, 85.0)
|
|
49
|
+
self.assertEqual(score.tier, "TRUSTED")
|
|
50
|
+
self.assertEqual(score.total_outcomes, 10)
|
|
51
|
+
self.assertEqual(len(score.category_scores), 1)
|
|
52
|
+
self.assertEqual(score.category_scores[0].category, "code-review")
|
|
53
|
+
|
|
54
|
+
@patch("urllib.request.urlopen")
|
|
55
|
+
def test_get_reputation_not_found(self, mock_open):
|
|
56
|
+
mock_open.side_effect = _http_error(404, {"error": "Agent not found"})
|
|
57
|
+
rep = AgentRep()
|
|
58
|
+
with self.assertRaises(NotFoundError):
|
|
59
|
+
rep.get_reputation("0xdeadbeef")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestSubmitOutcome(unittest.TestCase):
|
|
63
|
+
@patch("urllib.request.urlopen")
|
|
64
|
+
def test_submit_outcome_success(self, mock_open):
|
|
65
|
+
mock_open.return_value = _mock_response({
|
|
66
|
+
"outcomeId": "abc-123",
|
|
67
|
+
"status": "RESOLVED",
|
|
68
|
+
"verdict": "SUCCESS",
|
|
69
|
+
"llmJudgeReasoning": "Task completed correctly.",
|
|
70
|
+
"llmConfidence": 0.95,
|
|
71
|
+
"onChainTx": "0xtxhash",
|
|
72
|
+
"scoreImpact": 2.5,
|
|
73
|
+
})
|
|
74
|
+
rep = AgentRep(api_key="ar_test")
|
|
75
|
+
outcome = rep.submit_outcome(
|
|
76
|
+
contractor="0xA",
|
|
77
|
+
requester="0xB",
|
|
78
|
+
task="Write a hello world",
|
|
79
|
+
deliverable="print('hello world')",
|
|
80
|
+
category="code-review",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.assertEqual(outcome.verdict, "SUCCESS")
|
|
84
|
+
self.assertEqual(outcome.on_chain_tx, "0xtxhash")
|
|
85
|
+
|
|
86
|
+
def test_submit_outcome_requires_api_key(self):
|
|
87
|
+
rep = AgentRep() # no api_key
|
|
88
|
+
with self.assertRaises(AuthenticationError):
|
|
89
|
+
rep.submit_outcome("0xA", "0xB", "task", "deliverable")
|
|
90
|
+
|
|
91
|
+
@patch("urllib.request.urlopen")
|
|
92
|
+
def test_submit_outcome_validation_error(self, mock_open):
|
|
93
|
+
mock_open.side_effect = _http_error(400, {
|
|
94
|
+
"error": "Validation failed",
|
|
95
|
+
"fields": {"agentAddress": "Invalid EVM wallet address"},
|
|
96
|
+
})
|
|
97
|
+
rep = AgentRep(api_key="ar_test")
|
|
98
|
+
with self.assertRaises(ValidationError) as ctx:
|
|
99
|
+
rep.submit_outcome("invalid", "0xB", "task", "deliverable")
|
|
100
|
+
self.assertIn("agentAddress", ctx.exception.fields)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestRegister(unittest.TestCase):
|
|
104
|
+
@patch("urllib.request.urlopen")
|
|
105
|
+
def test_register_success(self, mock_open):
|
|
106
|
+
mock_open.return_value = _mock_response({
|
|
107
|
+
"agentId": "id-1",
|
|
108
|
+
"apiKey": "ar_abc123",
|
|
109
|
+
"walletAddress": "0xabc",
|
|
110
|
+
})
|
|
111
|
+
rep = AgentRep()
|
|
112
|
+
result = rep.register(
|
|
113
|
+
wallet_address="0xabc",
|
|
114
|
+
name="Test Agent",
|
|
115
|
+
categories=["code-review"],
|
|
116
|
+
)
|
|
117
|
+
self.assertEqual(result.api_key, "ar_abc123")
|
|
118
|
+
self.assertEqual(result.wallet_address, "0xabc")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestLeaderboard(unittest.TestCase):
|
|
122
|
+
@patch("urllib.request.urlopen")
|
|
123
|
+
def test_leaderboard(self, mock_open):
|
|
124
|
+
mock_open.return_value = _mock_response({
|
|
125
|
+
"content": [
|
|
126
|
+
{
|
|
127
|
+
"agentId": "1", "walletAddress": "0xA", "name": "Alpha",
|
|
128
|
+
"description": None, "score": 95.0, "tier": "ELITE",
|
|
129
|
+
"totalOutcomes": 50, "successRate": 0.98,
|
|
130
|
+
"categories": ["research"], "onChainVerified": True,
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"totalElements": 1, "number": 0, "size": 20, "totalPages": 1,
|
|
134
|
+
})
|
|
135
|
+
rep = AgentRep()
|
|
136
|
+
result = rep.leaderboard()
|
|
137
|
+
self.assertEqual(len(result.agents), 1)
|
|
138
|
+
self.assertEqual(result.agents[0].tier, "ELITE")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
unittest.main()
|