Skip to content

Commit cb9424a

Browse files
vvillait88claude
andauthored
feat: multi-chain API response support (v1.3.0) (#4)
## Summary - `subject.chain` (string) → `subject.chains` (string[]) - Per-chain data in `chains[]` array (score with confidence/dimensions, classification, identity, activity, evidence) - Top-level score is operator-level (no confidence/dimensions) - No top-level classification (per-chain only in `chains[]`) - Assess response includes `operator_score`, `reputation`, `agents` - Version bump to 1.3.0 - Updated deps ## Test plan - [ ] CI passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3beff85 commit cb9424a

5 files changed

Lines changed: 154 additions & 30 deletions

File tree

agentscore/types.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
class Subject(TypedDict):
11-
chain: str
11+
chains: list[str]
1212
address: str
1313

1414

@@ -23,12 +23,20 @@ class Classification(TypedDict):
2323

2424

2525
class Score(TypedDict):
26+
value: int | None
27+
grade: Grade | None
28+
scored_at: str | None
2629
status: ReputationStatus
30+
version: str
31+
32+
33+
class ChainScore(TypedDict):
2734
value: int | None
2835
grade: Grade | None
2936
confidence: float | None
3037
dimensions: dict[str, float] | None
3138
scored_at: str | None
39+
status: ReputationStatus
3240
version: str
3341

3442

@@ -105,13 +113,19 @@ class AgentSummary(TypedDict):
105113
grade: Grade
106114

107115

116+
class ChainEntry(TypedDict):
117+
chain: str
118+
score: ChainScore
119+
classification: Classification
120+
identity: Identity
121+
activity: Activity
122+
evidence_summary: EvidenceSummary
123+
124+
108125
class ReputationResponse(TypedDict):
109126
subject: Subject
110-
classification: Classification
111127
score: Score
112-
identity: Identity | None
113-
activity: Activity | None
114-
evidence_summary: EvidenceSummary | None
128+
chains: list[ChainEntry]
115129
data_semantics: str
116130
caveats: list[str]
117131
updated_at: str | None
@@ -131,17 +145,17 @@ class DecisionPolicy(TypedDict, total=False):
131145

132146
class AssessResponse(TypedDict):
133147
subject: Subject
134-
classification: Classification
135148
score: Score
136-
identity: Identity | None
137-
activity: Activity | None
138-
evidence_summary: EvidenceSummary | None
139-
data_semantics: str
140-
caveats: list[str]
141-
updated_at: str | None
149+
chains: list[ChainEntry]
142150
decision: str | None
143151
decision_reasons: list[str]
144152
on_the_fly: bool
153+
data_semantics: str
154+
caveats: list[str]
155+
updated_at: str | None
156+
operator_score: OperatorScore | None
157+
reputation: Reputation | None
158+
agents: list[AgentSummary]
145159

146160

147161
class AgentRecord(TypedDict):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "agentscore-py"
7-
version = "1.2.0"
7+
version = "1.3.0"
88
description = "Python client for the AgentScore trust and reputation API"
99
readme = "README.md"
1010
license = "MIT"

tests/test_client.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,24 @@ def test_constructor_default_timeout():
5858
# ---------------------------------------------------------------------------
5959

6060
REPUTATION_PAYLOAD = {
61-
"subject": {"chain": "base", "address": ADDRESS},
62-
"classification": {
63-
"entity_type": "wallet",
64-
"confidence": 0.9,
65-
"is_known": True,
66-
"is_known_erc8004_agent": False,
67-
"has_candidate_payment_activity": True,
68-
"has_verified_payment_activity": False,
69-
"reasons": [],
70-
},
61+
"subject": {"chains": ["base"], "address": ADDRESS},
7162
"score": {
72-
"status": "scored",
7363
"value": 75,
7464
"grade": "B",
75-
"confidence": 0.85,
76-
"dimensions": None,
7765
"scored_at": "2024-01-01T00:00:00Z",
66+
"status": "scored",
7867
"version": "1",
7968
},
80-
"identity": None,
81-
"activity": None,
82-
"evidence_summary": None,
69+
"chains": [
70+
{
71+
"chain": "base",
72+
"score": {"value": 75, "grade": "B"},
73+
"classification": {"entity_type": "wallet", "confidence": 0.9},
74+
"identity": {},
75+
"activity": {},
76+
"evidence_summary": {},
77+
},
78+
],
8379
"data_semantics": "live",
8480
"caveats": [],
8581
"updated_at": "2024-01-01T00:00:00Z",

tests/test_integration.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import os
2+
3+
import pytest
4+
5+
from agentscore import AgentScore
6+
7+
API_KEY = os.environ.get("AGENTSCORE_API_KEY")
8+
BASE_URL = os.environ.get("AGENTSCORE_BASE_URL", "http://api.dev.agentscore.internal")
9+
TEST_ADDRESS = "0x339559a2d1cd15059365fc7bd36b3047bba480e0"
10+
11+
pytestmark = pytest.mark.skipif(not API_KEY, reason="AGENTSCORE_API_KEY not set")
12+
13+
14+
def test_get_reputation_shape():
15+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
16+
rep = client.get_reputation(TEST_ADDRESS)
17+
18+
assert "chains" in rep["subject"]
19+
assert isinstance(rep["subject"]["chains"], list)
20+
assert len(rep["subject"]["chains"]) > 0
21+
22+
assert "value" in rep["score"]
23+
assert "grade" in rep["score"]
24+
assert "scored_at" in rep["score"]
25+
assert "status" in rep["score"]
26+
assert "version" in rep["score"]
27+
assert "confidence" not in rep["score"]
28+
assert "dimensions" not in rep["score"]
29+
30+
assert "chains" in rep
31+
assert isinstance(rep["chains"], list)
32+
assert len(rep["chains"]) > 0
33+
34+
chain = rep["chains"][0]
35+
assert "chain" in chain
36+
assert "score" in chain
37+
assert "classification" in chain
38+
assert "identity" in chain
39+
assert "activity" in chain
40+
assert "evidence_summary" in chain
41+
42+
assert "agents" in rep
43+
assert isinstance(rep["agents"], list)
44+
45+
46+
def test_get_reputation_chain_filter():
47+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
48+
rep = client.get_reputation(TEST_ADDRESS, chain="base")
49+
50+
assert rep["subject"]["chains"] == ["base"]
51+
assert len(rep["chains"]) == 1
52+
assert rep["chains"][0]["chain"] == "base"
53+
54+
55+
def test_get_reputation_chain_entry_full_fields():
56+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
57+
rep = client.get_reputation(TEST_ADDRESS)
58+
chain = rep["chains"][0]
59+
60+
assert "confidence" in chain["score"]
61+
assert "dimensions" in chain["score"]
62+
assert "as_verified_payer" in chain["activity"]
63+
assert "active_days" in chain["activity"]
64+
assert "first_candidate_tx_at" in chain["activity"]
65+
66+
67+
def test_get_reputation_metadata_fields():
68+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
69+
rep = client.get_reputation(TEST_ADDRESS)
70+
71+
assert "caveats" in rep
72+
assert "data_semantics" in rep
73+
assert "updated_at" in rep
74+
75+
76+
def test_assess_operator_level():
77+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
78+
result = client.assess(TEST_ADDRESS)
79+
80+
assert "decision" in result
81+
assert isinstance(result["decision_reasons"], list)
82+
assert isinstance(result["chains"], list)
83+
assert isinstance(result["agents"], list)
84+
assert "classification" not in result
85+
86+
87+
def test_assess_policy_deny():
88+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
89+
result = client.assess(TEST_ADDRESS, policy={"min_score": 999})
90+
91+
assert result["decision"] == "deny"
92+
assert len(result["decision_reasons"]) > 0
93+
94+
95+
def test_get_reputation_operator_score():
96+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
97+
rep = client.get_reputation(TEST_ADDRESS)
98+
op = rep.get("operator_score")
99+
if not op:
100+
pytest.skip("no operator_score on test address")
101+
assert isinstance(op["score"], int)
102+
assert isinstance(op["grade"], str)
103+
assert isinstance(op["agent_count"], int)
104+
assert isinstance(op["chains_active"], list)
105+
106+
107+
def test_get_reputation_reputation_field():
108+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
109+
rep = client.get_reputation(TEST_ADDRESS)
110+
r = rep.get("reputation")
111+
if not r:
112+
pytest.skip("no reputation on test address")
113+
assert isinstance(r["feedback_count"], int)
114+
assert isinstance(r["client_count"], int)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)