backloop-sdk 0.1.3__tar.gz → 0.1.5__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.
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/PKG-INFO +1 -1
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/pyproject.toml +1 -1
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/__init__.py +3 -1
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/_generated.py +3 -3
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/integrations.py +7 -2
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/server.py +34 -1
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_integrations.py +2 -1
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_server.py +10 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/.gitignore +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/LICENSE +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/README.md +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/_http.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/client.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/forward.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/ids.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/py.typed +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/redact.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/tool.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/types.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/src/backloop/validate.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/__init__.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/helpers.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_client.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_forward.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_ids.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_redact.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_tool.py +0 -0
- {backloop_sdk-0.1.3 → backloop_sdk-0.1.5}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: backloop-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Python SDK for the Agent Feedback Protocol: validate, send and receive structured feedback from AI agents.
|
|
5
5
|
Project-URL: Homepage, https://trybackloop.com
|
|
6
6
|
Project-URL: Specification, https://trybackloop.com/spec/
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "backloop-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "Python SDK for the Agent Feedback Protocol: validate, send and receive structured feedback from AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -16,6 +16,7 @@ from .server import (
|
|
|
16
16
|
FeedbackHandler,
|
|
17
17
|
HandlerResponse,
|
|
18
18
|
OnRecordResult,
|
|
19
|
+
agent_from_user_agent,
|
|
19
20
|
error_response,
|
|
20
21
|
feedback_link_header,
|
|
21
22
|
)
|
|
@@ -44,7 +45,7 @@ from .validate import (
|
|
|
44
45
|
validate_submission,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
|
-
__version__ = "0.1.
|
|
48
|
+
__version__ = "0.1.5"
|
|
48
49
|
|
|
49
50
|
__all__ = [
|
|
50
51
|
"ACK_SCHEMA",
|
|
@@ -77,6 +78,7 @@ __all__ = [
|
|
|
77
78
|
"SubmitResult",
|
|
78
79
|
"ValidationIssue",
|
|
79
80
|
"ValidationResult",
|
|
81
|
+
"agent_from_user_agent",
|
|
80
82
|
"error_response",
|
|
81
83
|
"fan_out",
|
|
82
84
|
"fastapi_router",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Generated by scripts/sync-schema.mjs from spec/. Do not edit by hand.
|
|
2
2
|
import json
|
|
3
3
|
|
|
4
|
-
FEEDBACK_SCHEMA = json.loads("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"$id\":\"https://trybackloop.com/schemas/agent-feedback/0.1/feedback.schema.json\",\"title\":\"Agent Feedback Submission\",\"description\":\"Structured feedback an AI agent sends to a service when something prevents or complicates its task. Agent Feedback Protocol v0.1.\",\"type\":\"object\",\"required\":[\"type\",\"goal\",\"message\"],\"additionalProperties\":false,\"properties\":{\"spec_version\":{\"description\":\"Protocol version the agent is speaking. Defaults to \\\"0.1\\\" when omitted.\",\"type\":\"string\",\"const\":\"0.1\"},\"type\":{\"description\":\"Category of the problem.\",\"type\":\"string\",\"enum\":[\"missing_capability\",\"bug\",\"unclear_documentation\",\"unexpected_response\",\"unhelpful_error\",\"performance\",\"other\"]},\"goal\":{\"description\":\"What the agent (or its user) was trying to accomplish, in plain language. Describe the task, not the API call.\",\"type\":\"string\",\"minLength\":1,\"maxLength\":1000},\"message\":{\"description\":\"What prevented or complicated the task.\",\"type\":\"string\",\"minLength\":1,\"maxLength\":4000},\"endpoint\":{\"description\":\"The operation involved: an HTTP path template (\\\"/companies/search\\\") or a tool name (\\\"search_companies\\\").\",\"type\":\"string\",\"minLength\":1,\"maxLength\":512},\"method\":{\"description\":\"HTTP method, when the endpoint is an HTTP path.\",\"type\":\"string\",\"enum\":[\"GET\",\"POST\",\"PUT\",\"PATCH\",\"DELETE\",\"HEAD\",\"OPTIONS\"]},\"outcome\":{\"description\":\"What happened to the task. blocked: the task could not be completed. degraded: completed, but worse or slower than it should have been. completed: completed fine; the feedback is a suggestion.\",\"type\":\"string\",\"enum\":[\"blocked\",\"degraded\",\"completed\"]},\"workaround\":{\"description\":\"Whether the agent found a workaround.\",\"type\":\"boolean\"},\"workaround_description\":{\"description\":\"The workaround the agent used, if any.\",\"type\":\"string\",\"maxLength\":2000},\"expected\":{\"description\":\"What the agent expected to find or happen.\",\"type\":\"string\",\"maxLength\":2000},\"suggestion\":{\"description\":\"A concrete change that would have let the agent succeed.\",\"type\":\"string\",\"maxLength\":2000},\"request_id\":{\"description\":\"The service's request ID for a related call, to join feedback with logs.\",\"type\":\"string\",\"maxLength\":256},\"session_id\":{\"description\":\"Opaque ID shared by all feedback from one agent task run. Used to de-duplicate retries.\",\"type\":\"string\",\"maxLength\":256},\"agent\":{\"description\":\"Self-reported
|
|
4
|
+
FEEDBACK_SCHEMA = json.loads("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"$id\":\"https://trybackloop.com/schemas/agent-feedback/0.1/feedback.schema.json\",\"title\":\"Agent Feedback Submission\",\"description\":\"Structured feedback an AI agent sends to a service when something prevents or complicates its task. Agent Feedback Protocol v0.1.\",\"type\":\"object\",\"required\":[\"type\",\"goal\",\"message\"],\"additionalProperties\":false,\"properties\":{\"spec_version\":{\"description\":\"Protocol version the agent is speaking. Defaults to \\\"0.1\\\" when omitted.\",\"type\":\"string\",\"const\":\"0.1\"},\"type\":{\"description\":\"Category of the problem.\",\"type\":\"string\",\"enum\":[\"missing_capability\",\"bug\",\"unclear_documentation\",\"unexpected_response\",\"unhelpful_error\",\"performance\",\"other\"]},\"goal\":{\"description\":\"What the agent (or its user) was trying to accomplish, in plain language. Describe the task, not the API call.\",\"type\":\"string\",\"minLength\":1,\"maxLength\":1000},\"message\":{\"description\":\"What prevented or complicated the task.\",\"type\":\"string\",\"minLength\":1,\"maxLength\":4000},\"endpoint\":{\"description\":\"The operation involved: an HTTP path template (\\\"/companies/search\\\") or a tool name (\\\"search_companies\\\").\",\"type\":\"string\",\"minLength\":1,\"maxLength\":512},\"method\":{\"description\":\"HTTP method, when the endpoint is an HTTP path.\",\"type\":\"string\",\"enum\":[\"GET\",\"POST\",\"PUT\",\"PATCH\",\"DELETE\",\"HEAD\",\"OPTIONS\"]},\"outcome\":{\"description\":\"What happened to the task. blocked: the task could not be completed. degraded: completed, but worse or slower than it should have been. completed: completed fine; the feedback is a suggestion.\",\"type\":\"string\",\"enum\":[\"blocked\",\"degraded\",\"completed\"]},\"workaround\":{\"description\":\"Whether the agent found a workaround.\",\"type\":\"boolean\"},\"workaround_description\":{\"description\":\"The workaround the agent used, if any.\",\"type\":\"string\",\"maxLength\":2000},\"expected\":{\"description\":\"What the agent expected to find or happen.\",\"type\":\"string\",\"maxLength\":2000},\"suggestion\":{\"description\":\"A concrete change that would have let the agent succeed.\",\"type\":\"string\",\"maxLength\":2000},\"request_id\":{\"description\":\"The service's request ID for a related call, to join feedback with logs.\",\"type\":\"string\",\"maxLength\":256},\"session_id\":{\"description\":\"Opaque ID shared by all feedback from one agent task run. Used to de-duplicate retries.\",\"type\":\"string\",\"maxLength\":256},\"agent\":{\"description\":\"Who is reporting: the agent or product name and its model. Self-reported; always fill `name` and `model`.\",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"name\":{\"type\":\"string\",\"maxLength\":128},\"version\":{\"type\":\"string\",\"maxLength\":64},\"model\":{\"type\":\"string\",\"maxLength\":128},\"framework\":{\"type\":\"string\",\"maxLength\":128}}},\"evidence\":{\"description\":\"Sanitized details of the failing interaction. Never include credentials or personal data.\",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"status_code\":{\"type\":\"integer\",\"minimum\":100,\"maximum\":599},\"request\":{\"description\":\"Parameters or body the agent sent, with secrets removed.\",\"type\":\"object\"},\"response_excerpt\":{\"description\":\"The relevant part of the response.\",\"type\":\"string\",\"maxLength\":4000}}},\"metadata\":{\"description\":\"Free-form extra data. Services may ignore it.\",\"type\":\"object\"}}}")
|
|
5
5
|
|
|
6
6
|
RECORD_SCHEMA = json.loads("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"$id\":\"https://trybackloop.com/schemas/agent-feedback/0.1/record.schema.json\",\"title\":\"Agent Feedback Record\",\"description\":\"A feedback submission after the receiving service has accepted it: the agent's submission plus server-side context. This is what services store and forward to collectors.\",\"type\":\"object\",\"required\":[\"id\",\"received_at\",\"feedback\"],\"additionalProperties\":false,\"properties\":{\"id\":{\"description\":\"Unique ID assigned by the receiving service.\",\"type\":\"string\",\"minLength\":1,\"maxLength\":128},\"received_at\":{\"description\":\"RFC 3339 timestamp of when the service accepted the submission.\",\"type\":\"string\",\"format\":\"date-time\"},\"service\":{\"description\":\"Name of the service that received the feedback.\",\"type\":\"string\",\"maxLength\":128},\"account\":{\"description\":\"Opaque, stable identifier of the customer account the agent authenticated as. Services should hash or pseudonymize it.\",\"type\":\"string\",\"maxLength\":256},\"source\":{\"description\":\"Channel the feedback arrived through.\",\"type\":\"string\",\"enum\":[\"http\",\"mcp\",\"sdk\",\"other\"]},\"feedback\":{\"$ref\":\"feedback.schema.json\"}}}")
|
|
7
7
|
|
|
8
8
|
ACK_SCHEMA = json.loads("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"$id\":\"https://trybackloop.com/schemas/agent-feedback/0.1/ack.schema.json\",\"title\":\"Agent Feedback Acknowledgement\",\"description\":\"Body of a 202 response to POST /feedback.\",\"type\":\"object\",\"required\":[\"id\",\"status\",\"received_at\"],\"properties\":{\"id\":{\"type\":\"string\"},\"status\":{\"type\":\"string\",\"const\":\"accepted\"},\"received_at\":{\"type\":\"string\",\"format\":\"date-time\"},\"known_issue\":{\"description\":\"Present when the service already tracks this problem. Agents can use it to stop retrying or to apply a known workaround.\",\"type\":\"object\",\"required\":[\"id\",\"title\",\"status\"],\"properties\":{\"id\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"status\":{\"type\":\"string\",\"enum\":[\"acknowledged\",\"planned\",\"in_progress\",\"fixed\",\"wont_fix\"]},\"url\":{\"type\":\"string\"},\"workaround\":{\"type\":\"string\"}}}}}")
|
|
9
9
|
|
|
10
|
-
AGENT_FEEDBACK_INSTRUCTIONS = "## Reporting feedback to the services you use\n\nThe services you call accept structured feedback through `POST /feedback` (or the `submit_feedback` tool). Your reports go to the engineers who build the service and are used to fix what got in your way. Use it.\n\n**Submit feedback when something prevented or complicated your task:**\n- a capability you needed does not exist (a filter, field, endpoint or bulk operation);\n- the documentation was missing, ambiguous or wrong, and you had to guess;\n- a response was surprising or inconsistent with the docs;\n- an error message did not tell you how to fix the request;\n- the service behaved incorrectly;\n- latency, rate limits or pagination made the task impractical.\n\n**Don't submit feedback** for your own mistakes that the service reported clearly, for things you have not tried, or more than once for the same problem in the same task.\n\n**Write it like this:**\n- `goal`: what you or your user were trying to accomplish, in plain language. \"Find companies currently hiring GTM engineers\", not \"call /companies/search\".\n- `message`: what got in the way, specifically. \"No hiring-role filter is available; `q` only matches company names.\"\n- `type`: `missing_capability`, `bug`, `unclear_documentation`, `unexpected_response`, `unhelpful_error`, `performance` or `other`.\n- Fill `endpoint`, `outcome` (`blocked`, `degraded` or `completed`), `workaround`, `expected` and `suggestion` when you know them. A concrete suggestion is the most useful thing you can give.\n- Reuse the same `session_id` for every report in one task.\n\n**Never include** credentials, API keys, tokens, passwords, personal data, or your user's private content. Describe the shape of a request, not its secrets.\n\n**Keep going.** Submitting feedback is best effort. Send it, then continue with the task or a workaround. If the response contains `known_issue`, the problem is already tracked: tell your user if it matters, and use the workaround if one is given."
|
|
10
|
+
AGENT_FEEDBACK_INSTRUCTIONS = "## Reporting feedback to the services you use\n\nThe services you call accept structured feedback through `POST /feedback` (or the `submit_feedback` tool). Your reports go to the engineers who build the service and are used to fix what got in your way. Use it.\n\n**Submit feedback when something prevented or complicated your task, even if you found a workaround:**\n- a capability you needed does not exist (a filter, field, endpoint or bulk operation), so you made extra calls, filtered results yourself, or gave up;\n- the documentation was missing, ambiguous or wrong, and you had to guess;\n- a response was surprising or inconsistent with the docs;\n- an error message did not tell you how to fix the request;\n- the service behaved incorrectly;\n- latency, rate limits or pagination made the task impractical.\n\n**Before your final answer,** check whether any of these happened. If one did and you have not reported it, report it now, then answer.\n\n**Don't submit feedback** for your own mistakes that the service reported clearly, for things you have not tried, or more than once for the same problem in the same task.\n\n**Write it like this:**\n- `goal`: what you or your user were trying to accomplish, in plain language. \"Find companies currently hiring GTM engineers\", not \"call /companies/search\".\n- `message`: what got in the way, specifically. \"No hiring-role filter is available; `q` only matches company names.\"\n- `type`: `missing_capability`, `bug`, `unclear_documentation`, `unexpected_response`, `unhelpful_error`, `performance` or `other`.\n- Fill `endpoint`, `outcome` (`blocked`, `degraded` or `completed`), `workaround`, `expected` and `suggestion` when you know them. A concrete suggestion is the most useful thing you can give.\n- Always fill `agent`: `name` (the agent or product you run as) and `model`. Engineers use it to tell which agent a report came from.\n- Reuse the same `session_id` for every report in one task.\n\n**Never include** credentials, API keys, tokens, passwords, personal data, or your user's private content. Describe the shape of a request, not its secrets.\n\n**Keep going.** Submitting feedback is best effort. Send it, then continue with the task or a workaround. If the response contains `known_issue`, the problem is already tracked: tell your user if it matters, and use the workaround if one is given."
|
|
11
11
|
|
|
12
|
-
FEEDBACK_TOOL_DESCRIPTION = "Report a problem with this service to its engineering team. Call this when something prevented or complicated your task: a missing capability (filter, field, endpoint), unclear or wrong documentation, a surprising response, an unhelpful error, a bug, or impractical performance. Describe your goal in plain language, say specifically what got in the way, and include a concrete suggestion if you have one. Report each distinct problem once per task. Never include credentials, personal data or your user's private content. This is best effort: after calling it, continue with your task."
|
|
12
|
+
FEEDBACK_TOOL_DESCRIPTION = "Report a problem with this service to its engineering team. Call this when something prevented or complicated your task: a missing capability (filter, field, endpoint), unclear or wrong documentation, a surprising response, an unhelpful error, a bug, or impractical performance. Describe your goal in plain language, say specifically what got in the way, and include a concrete suggestion if you have one. Say who you are in `agent` (name and model). Report each distinct problem once per task. Never include credentials, personal data or your user's private content. This is best effort: after calling it, continue with your task."
|
|
@@ -65,7 +65,12 @@ def fastapi_router(handler: FeedbackHandler, identify: Optional[Identify] = None
|
|
|
65
65
|
request.headers.get("x-forwarded-for"), request.client.host if request.client else None
|
|
66
66
|
)
|
|
67
67
|
status, payload, headers = await run_in_threadpool(
|
|
68
|
-
handler.handle_submit,
|
|
68
|
+
handler.handle_submit,
|
|
69
|
+
bytes(body),
|
|
70
|
+
request.headers.get("content-type"),
|
|
71
|
+
account,
|
|
72
|
+
client_ip,
|
|
73
|
+
request.headers.get("user-agent"),
|
|
69
74
|
)
|
|
70
75
|
return JSONResponse(payload, status_code=status, headers=headers)
|
|
71
76
|
|
|
@@ -94,7 +99,7 @@ def flask_blueprint(handler: FeedbackHandler, identify: Optional[Identify] = Non
|
|
|
94
99
|
account = resolve(request) if resolve else None
|
|
95
100
|
client_ip = _client_ip(request.headers.get("X-Forwarded-For"), request.remote_addr)
|
|
96
101
|
status, payload, headers = handler.handle_submit(
|
|
97
|
-
body, request.headers.get("Content-Type"), account, client_ip
|
|
102
|
+
body, request.headers.get("Content-Type"), account, client_ip, request.headers.get("User-Agent")
|
|
98
103
|
)
|
|
99
104
|
return Response(json.dumps(payload, ensure_ascii=False), status=status, headers=headers)
|
|
100
105
|
|
|
@@ -38,6 +38,29 @@ JSON_HEADERS = {"content-type": "application/json; charset=utf-8"}
|
|
|
38
38
|
_JSON_CONTENT_TYPE = re.compile(r"application/(?:[\w.+-]+\+)?json\b", re.I | re.ASCII)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
#: HTTP libraries, browsers and the SDK clients: a User-Agent that says nothing about the agent.
|
|
42
|
+
_GENERIC_USER_AGENT = re.compile(
|
|
43
|
+
r"(mozilla|backloop-sdk|python|curl|wget|node|undici|axios|got|go-http-client|okhttp|java"
|
|
44
|
+
r"|apache-httpclient|aiohttp|werkzeug|ruby|deno|bun|postmanruntime|libwww)",
|
|
45
|
+
re.I,
|
|
46
|
+
)
|
|
47
|
+
_PRODUCT_TOKEN = re.compile(r"([^\s/()]{1,128})(?:/([^\s()]{1,64}))?")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def agent_from_user_agent(user_agent: Optional[str]) -> Optional[Dict[str, str]]:
|
|
51
|
+
"""An ``agent`` block from a ``User-Agent`` header (its first product token).
|
|
52
|
+
|
|
53
|
+
None when the header only names an HTTP library or a browser.
|
|
54
|
+
"""
|
|
55
|
+
m = _PRODUCT_TOKEN.match((user_agent or "").strip())
|
|
56
|
+
if not m or _GENERIC_USER_AGENT.match(m.group(1)):
|
|
57
|
+
return None
|
|
58
|
+
agent = {"name": m.group(1)}
|
|
59
|
+
if m.group(2):
|
|
60
|
+
agent["version"] = m.group(2)
|
|
61
|
+
return agent
|
|
62
|
+
|
|
63
|
+
|
|
41
64
|
def error_response(
|
|
42
65
|
status: int,
|
|
43
66
|
code: str,
|
|
@@ -134,8 +157,13 @@ class FeedbackHandler:
|
|
|
134
157
|
content_type: Optional[str],
|
|
135
158
|
account: Optional[str] = None,
|
|
136
159
|
client_ip: Optional[str] = None,
|
|
160
|
+
user_agent: Optional[str] = None,
|
|
137
161
|
) -> HandlerResponse:
|
|
138
|
-
"""Handle ``POST /feedback``. Returns ``(status, json_body, headers)``.
|
|
162
|
+
"""Handle ``POST /feedback``. Returns ``(status, json_body, headers)``.
|
|
163
|
+
|
|
164
|
+
``user_agent`` is the request's ``User-Agent`` header: it names the agent
|
|
165
|
+
on submissions that have no ``agent.name``.
|
|
166
|
+
"""
|
|
139
167
|
raw = body.encode("utf-8") if isinstance(body, str) else bytes(body)
|
|
140
168
|
if not _JSON_CONTENT_TYPE.match(content_type or ""):
|
|
141
169
|
return error_response(415, "unsupported_media_type", "Content-Type must be application/json")
|
|
@@ -171,6 +199,11 @@ class FeedbackHandler:
|
|
|
171
199
|
record["source"] = self.source or "http"
|
|
172
200
|
if self.redact is not False:
|
|
173
201
|
parsed = _redact(parsed, **(self.redact if isinstance(self.redact, Mapping) else {}))
|
|
202
|
+
# Agents often skip `agent`: the User-Agent may still say who is calling.
|
|
203
|
+
if not (parsed.get("agent") or {}).get("name"):
|
|
204
|
+
seen = agent_from_user_agent(user_agent)
|
|
205
|
+
if seen:
|
|
206
|
+
parsed = {**parsed, "agent": {**seen, **(parsed.get("agent") or {})}}
|
|
174
207
|
record["feedback"] = parsed
|
|
175
208
|
|
|
176
209
|
try:
|
|
@@ -41,7 +41,8 @@ class FastAPITest(unittest.TestCase):
|
|
|
41
41
|
res = self.client(handler).post("/feedback", json=BODY)
|
|
42
42
|
self.assertEqual(res.status_code, 202)
|
|
43
43
|
self.assertEqual(res.json()["known_issue"], KNOWN_ISSUE)
|
|
44
|
-
|
|
44
|
+
# The submission names no agent: the caller's User-Agent does (Starlette's test client).
|
|
45
|
+
self.assertEqual(sink.records[0]["feedback"], {**BODY, "agent": {"name": "testclient"}})
|
|
45
46
|
|
|
46
47
|
def test_identify_sync_and_async(self):
|
|
47
48
|
async def async_identify(request):
|
|
@@ -72,6 +72,16 @@ class HappyPathTest(unittest.TestCase):
|
|
|
72
72
|
self.assertEqual(list(record), ["id", "received_at", "service", "account", "source", "feedback"])
|
|
73
73
|
self.assertTrue(validate_record(record).valid)
|
|
74
74
|
|
|
75
|
+
def test_names_the_agent_from_the_user_agent(self):
|
|
76
|
+
handler, sink = make()
|
|
77
|
+
handler.handle_submit(BODY, JSON, user_agent="claude-code/2.1.0 (external, cli)")
|
|
78
|
+
named = json.dumps({**json.loads(BODY), "agent": {"name": "ops-agent"}}).encode()
|
|
79
|
+
handler.handle_submit(named, JSON, user_agent="claude-code/2.1.0")
|
|
80
|
+
# An HTTP library is not an agent.
|
|
81
|
+
handler.handle_submit(BODY, JSON, user_agent="python-requests/2.32.3")
|
|
82
|
+
agents = [r["feedback"].get("agent") for r in sink.records]
|
|
83
|
+
self.assertEqual(agents, [{"name": "claude-code", "version": "2.1.0"}, {"name": "ops-agent"}, None])
|
|
84
|
+
|
|
75
85
|
def test_known_issue_is_passed_back(self):
|
|
76
86
|
handler, _ = make(lambda record: {"known_issue": KNOWN_ISSUE})
|
|
77
87
|
status, body, _ = handler.handle_submit(BODY, JSON)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|