agentsnap 0.1.0__py3-none-any.whl
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.
- agentsnap/__init__.py +23 -0
- agentsnap/adapters/__init__.py +0 -0
- agentsnap/adapters/anthropic.py +51 -0
- agentsnap/adapters/cohere.py +59 -0
- agentsnap/adapters/google.py +64 -0
- agentsnap/adapters/groq.py +19 -0
- agentsnap/adapters/langgraph.py +94 -0
- agentsnap/adapters/mistral.py +65 -0
- agentsnap/adapters/openai.py +60 -0
- agentsnap/adapters/openrouter.py +28 -0
- agentsnap/adapters/tool.py +36 -0
- agentsnap/cli.py +225 -0
- agentsnap/config.py +158 -0
- agentsnap/core/__init__.py +0 -0
- agentsnap/core/asserter.py +107 -0
- agentsnap/core/diff.py +345 -0
- agentsnap/core/recorder.py +79 -0
- agentsnap/core/snapshot.py +77 -0
- agentsnap/exceptions.py +48 -0
- agentsnap/patches.py +236 -0
- agentsnap/pytest_plugin.py +257 -0
- agentsnap/setup_wizard.py +230 -0
- agentsnap/wrap.py +105 -0
- agentsnap-0.1.0.dist-info/METADATA +456 -0
- agentsnap-0.1.0.dist-info/RECORD +27 -0
- agentsnap-0.1.0.dist-info/WHEEL +4 -0
- agentsnap-0.1.0.dist-info/entry_points.txt +5 -0
agentsnap/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from agentsnap import config
|
|
2
|
+
from agentsnap.core.asserter import AgentAsserter
|
|
3
|
+
from agentsnap.core.diff import LLMJudge
|
|
4
|
+
from agentsnap.core.recorder import AgentRecorder
|
|
5
|
+
from agentsnap.exceptions import (
|
|
6
|
+
AdapterNotWrappedError,
|
|
7
|
+
AgentRegressionError,
|
|
8
|
+
SnapshotNotFoundError,
|
|
9
|
+
)
|
|
10
|
+
from agentsnap.patches import PatchSet
|
|
11
|
+
from agentsnap.wrap import wrap
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"wrap",
|
|
15
|
+
"AgentRecorder",
|
|
16
|
+
"AgentAsserter",
|
|
17
|
+
"LLMJudge",
|
|
18
|
+
"PatchSet",
|
|
19
|
+
"AgentRegressionError",
|
|
20
|
+
"SnapshotNotFoundError",
|
|
21
|
+
"AdapterNotWrappedError",
|
|
22
|
+
"config",
|
|
23
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _MessagesProxy:
|
|
7
|
+
def __init__(self, original) -> None:
|
|
8
|
+
self._original = original
|
|
9
|
+
|
|
10
|
+
def create(self, **kwargs):
|
|
11
|
+
acc = TraceAccumulator.current()
|
|
12
|
+
if acc is None:
|
|
13
|
+
return self._original.create(**kwargs)
|
|
14
|
+
|
|
15
|
+
messages = kwargs.get("messages", [])
|
|
16
|
+
response = self._original.create(**kwargs)
|
|
17
|
+
|
|
18
|
+
response_text = ""
|
|
19
|
+
tokens = 0
|
|
20
|
+
if hasattr(response, "content"):
|
|
21
|
+
for block in response.content:
|
|
22
|
+
if hasattr(block, "text"):
|
|
23
|
+
response_text += block.text
|
|
24
|
+
if hasattr(response, "usage"):
|
|
25
|
+
tokens = getattr(response.usage, "input_tokens", 0) + getattr(
|
|
26
|
+
response.usage, "output_tokens", 0
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
acc.push(
|
|
30
|
+
{
|
|
31
|
+
"type": "llm_call",
|
|
32
|
+
"messages": messages,
|
|
33
|
+
"response": response_text,
|
|
34
|
+
"tokens": tokens,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return response
|
|
38
|
+
|
|
39
|
+
def __getattr__(self, name: str):
|
|
40
|
+
return getattr(self._original, name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AnthropicAdapter:
|
|
44
|
+
"""Wraps an anthropic.Anthropic() client to intercept .messages.create()."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, client) -> None:
|
|
47
|
+
self._client = client
|
|
48
|
+
self.messages = _MessagesProxy(client.messages)
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, name: str):
|
|
51
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _CohereV2ChatProxy:
|
|
7
|
+
"""Intercepts cohere.ClientV2().chat()."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, original_chat_fn) -> None:
|
|
10
|
+
self._original = original_chat_fn
|
|
11
|
+
|
|
12
|
+
def __call__(self, **kwargs):
|
|
13
|
+
acc = TraceAccumulator.current()
|
|
14
|
+
if acc is None:
|
|
15
|
+
return self._original(**kwargs)
|
|
16
|
+
|
|
17
|
+
messages = kwargs.get("messages", [])
|
|
18
|
+
response = self._original(**kwargs)
|
|
19
|
+
|
|
20
|
+
response_text = ""
|
|
21
|
+
tokens = 0
|
|
22
|
+
if hasattr(response, "message") and hasattr(response.message, "content"):
|
|
23
|
+
for block in response.message.content:
|
|
24
|
+
if hasattr(block, "text"):
|
|
25
|
+
response_text += block.text
|
|
26
|
+
if hasattr(response, "usage"):
|
|
27
|
+
tokens = getattr(response.usage, "tokens", None)
|
|
28
|
+
if tokens is None:
|
|
29
|
+
inp = getattr(response.usage, "input_tokens", 0) or 0
|
|
30
|
+
out = getattr(response.usage, "output_tokens", 0) or 0
|
|
31
|
+
tokens = inp + out
|
|
32
|
+
|
|
33
|
+
acc.push(
|
|
34
|
+
{
|
|
35
|
+
"type": "llm_call",
|
|
36
|
+
"messages": messages,
|
|
37
|
+
"response": response_text,
|
|
38
|
+
"tokens": tokens or 0,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CohereAdapter:
|
|
45
|
+
"""Wraps a cohere.ClientV2() to intercept .chat().
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
import cohere
|
|
49
|
+
from agentsnap.adapters.cohere import CohereAdapter
|
|
50
|
+
client = CohereAdapter(cohere.ClientV2(api_key="..."))
|
|
51
|
+
response = client.chat(model="command-r-plus", messages=[...])
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client) -> None:
|
|
55
|
+
self._client = client
|
|
56
|
+
self.chat = _CohereV2ChatProxy(client.chat)
|
|
57
|
+
|
|
58
|
+
def __getattr__(self, name: str):
|
|
59
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _ModelsProxy:
|
|
7
|
+
def __init__(self, original) -> None:
|
|
8
|
+
self._original = original
|
|
9
|
+
|
|
10
|
+
def generate_content(self, model: str, contents, **kwargs):
|
|
11
|
+
acc = TraceAccumulator.current()
|
|
12
|
+
if acc is None:
|
|
13
|
+
return self._original.generate_content(model=model, contents=contents, **kwargs)
|
|
14
|
+
|
|
15
|
+
if isinstance(contents, str):
|
|
16
|
+
messages = [{"role": "user", "content": contents}]
|
|
17
|
+
elif isinstance(contents, list):
|
|
18
|
+
messages = contents
|
|
19
|
+
else:
|
|
20
|
+
messages = [{"role": "user", "content": str(contents)}]
|
|
21
|
+
|
|
22
|
+
response = self._original.generate_content(model=model, contents=contents, **kwargs)
|
|
23
|
+
|
|
24
|
+
response_text = ""
|
|
25
|
+
tokens = 0
|
|
26
|
+
if hasattr(response, "text"):
|
|
27
|
+
response_text = response.text or ""
|
|
28
|
+
elif hasattr(response, "candidates") and response.candidates:
|
|
29
|
+
for part in response.candidates[0].content.parts:
|
|
30
|
+
if hasattr(part, "text"):
|
|
31
|
+
response_text += part.text
|
|
32
|
+
if hasattr(response, "usage_metadata"):
|
|
33
|
+
tokens = getattr(response.usage_metadata, "total_token_count", 0)
|
|
34
|
+
|
|
35
|
+
acc.push(
|
|
36
|
+
{
|
|
37
|
+
"type": "llm_call",
|
|
38
|
+
"messages": messages,
|
|
39
|
+
"response": response_text,
|
|
40
|
+
"tokens": tokens,
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
return response
|
|
44
|
+
|
|
45
|
+
def __getattr__(self, name: str):
|
|
46
|
+
return getattr(self._original, name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GeminiAdapter:
|
|
50
|
+
"""Wraps a google.genai.Client() to intercept .models.generate_content().
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
from google import genai
|
|
54
|
+
from agentsnap.adapters.google import GeminiAdapter
|
|
55
|
+
client = GeminiAdapter(genai.Client(api_key="..."))
|
|
56
|
+
response = client.models.generate_content(model="gemini-2.0-flash", contents="Hello")
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, client) -> None:
|
|
60
|
+
self._client = client
|
|
61
|
+
self.models = _ModelsProxy(client.models)
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, name: str):
|
|
64
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.adapters.openai import OpenAIAdapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GroqAdapter(OpenAIAdapter):
|
|
7
|
+
"""Wraps a groq.Groq() client (OpenAI-compatible interface).
|
|
8
|
+
|
|
9
|
+
Groq uses the same .chat.completions.create() interface as OpenAI,
|
|
10
|
+
so this is a thin alias over OpenAIAdapter.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from groq import Groq
|
|
14
|
+
from agentsnap.adapters.groq import GroqAdapter
|
|
15
|
+
client = GroqAdapter(Groq(api_key="..."))
|
|
16
|
+
response = client.chat.completions.create(
|
|
17
|
+
model="llama-3.3-70b-versatile", messages=[...]
|
|
18
|
+
)
|
|
19
|
+
"""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from langchain_core.callbacks import BaseCallbackHandler as _Base
|
|
7
|
+
except ImportError:
|
|
8
|
+
_Base = object # type: ignore[assignment,misc]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentSnapCallback(_Base):
|
|
12
|
+
"""LangChain callback handler that records LLM and tool events into TraceAccumulator.
|
|
13
|
+
|
|
14
|
+
Injected automatically by LangGraphAdapter. Also works standalone with any
|
|
15
|
+
LangChain model or chain that supports the callbacks API.
|
|
16
|
+
|
|
17
|
+
When langchain_core is not installed, _Base is object — the class is still
|
|
18
|
+
importable and its methods callable; it just won't be registered as a real
|
|
19
|
+
LangChain callback (duck-typing makes it work with our mock graphs in tests).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def on_llm_end(self, response, **kwargs) -> None:
|
|
23
|
+
acc = TraceAccumulator.current()
|
|
24
|
+
if acc is None:
|
|
25
|
+
return
|
|
26
|
+
text = ""
|
|
27
|
+
if hasattr(response, "generations") and response.generations:
|
|
28
|
+
gen = response.generations[0][0]
|
|
29
|
+
if hasattr(gen, "text") and gen.text:
|
|
30
|
+
text = gen.text
|
|
31
|
+
elif hasattr(gen, "message") and hasattr(gen.message, "content"):
|
|
32
|
+
text = gen.message.content or ""
|
|
33
|
+
else:
|
|
34
|
+
text = str(gen)
|
|
35
|
+
acc.push({"type": "llm_call", "messages": [], "response": text, "tokens": 0})
|
|
36
|
+
|
|
37
|
+
def on_tool_end(self, output, *, name: str = "", **kwargs) -> None:
|
|
38
|
+
acc = TraceAccumulator.current()
|
|
39
|
+
if acc is None:
|
|
40
|
+
return
|
|
41
|
+
acc.push({"type": "tool_call", "name": name, "args": {}, "result": str(output)})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LangGraphAdapter:
|
|
45
|
+
"""Wraps a CompiledGraph to capture traces via the LangChain callbacks system.
|
|
46
|
+
|
|
47
|
+
Injects AgentSnapCallback into every invoke() call so that LLM and tool
|
|
48
|
+
events from individual graph nodes are recorded into the active
|
|
49
|
+
TraceAccumulator.
|
|
50
|
+
|
|
51
|
+
Falls back to recording the top-level result as a single llm_call event
|
|
52
|
+
when langchain_core is not installed (no callbacks support available).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, graph) -> None:
|
|
56
|
+
self._graph = graph
|
|
57
|
+
|
|
58
|
+
def invoke(self, input_data, **kwargs):
|
|
59
|
+
acc = TraceAccumulator.current()
|
|
60
|
+
if acc is None:
|
|
61
|
+
return self._graph.invoke(input_data, **kwargs)
|
|
62
|
+
|
|
63
|
+
# Inject AgentSnapCallback via config so node-level events are captured.
|
|
64
|
+
# AgentSnapCallback is always usable via duck-typing even without langchain_core.
|
|
65
|
+
# When langchain_core is absent (_Base is object), we still inject the callback
|
|
66
|
+
# so that graphs that accept a callbacks list (e.g. LangGraph) fire it correctly.
|
|
67
|
+
# The top-level fallback push only runs when langchain_core is absent, because
|
|
68
|
+
# in that case a real CompiledGraph won't know to call on_llm_end/on_tool_end.
|
|
69
|
+
config = dict(kwargs.pop("config", None) or {})
|
|
70
|
+
callbacks = list(config.get("callbacks") or [])
|
|
71
|
+
callbacks.append(AgentSnapCallback())
|
|
72
|
+
config["callbacks"] = callbacks
|
|
73
|
+
|
|
74
|
+
if _Base is not object:
|
|
75
|
+
# langchain_core available: callback will be invoked by the real runtime
|
|
76
|
+
return self._graph.invoke(input_data, config=config, **kwargs)
|
|
77
|
+
|
|
78
|
+
# Fallback: langchain_core absent — real CompiledGraph won't fire callbacks,
|
|
79
|
+
# so record the top-level invocation as a single llm_call event instead.
|
|
80
|
+
result = self._graph.invoke(input_data, config=config, **kwargs)
|
|
81
|
+
if not any(e["type"] == "llm_call" for e in acc.trace):
|
|
82
|
+
acc.push({
|
|
83
|
+
"type": "llm_call",
|
|
84
|
+
"messages": [{"role": "user", "content": str(input_data)}],
|
|
85
|
+
"response": str(result),
|
|
86
|
+
"tokens": 0,
|
|
87
|
+
})
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
def stream(self, input_data, **kwargs):
|
|
91
|
+
return self._graph.stream(input_data, **kwargs)
|
|
92
|
+
|
|
93
|
+
def __getattr__(self, name: str):
|
|
94
|
+
return getattr(self._graph, name)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _MistralCompletionsProxy:
|
|
7
|
+
def __init__(self, original) -> None:
|
|
8
|
+
self._original = original
|
|
9
|
+
|
|
10
|
+
def complete(self, **kwargs):
|
|
11
|
+
acc = TraceAccumulator.current()
|
|
12
|
+
if acc is None:
|
|
13
|
+
return self._original.complete(**kwargs)
|
|
14
|
+
|
|
15
|
+
messages = kwargs.get("messages", [])
|
|
16
|
+
kwargs["stream"] = False
|
|
17
|
+
response = self._original.complete(**kwargs)
|
|
18
|
+
|
|
19
|
+
response_text = ""
|
|
20
|
+
tokens = 0
|
|
21
|
+
if hasattr(response, "choices") and response.choices:
|
|
22
|
+
msg = response.choices[0].message
|
|
23
|
+
response_text = msg.content or ""
|
|
24
|
+
if hasattr(response, "usage"):
|
|
25
|
+
tokens = getattr(response.usage, "total_tokens", 0) or 0
|
|
26
|
+
|
|
27
|
+
acc.push(
|
|
28
|
+
{
|
|
29
|
+
"type": "llm_call",
|
|
30
|
+
"messages": messages,
|
|
31
|
+
"response": response_text,
|
|
32
|
+
"tokens": tokens,
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, name: str):
|
|
38
|
+
return getattr(self._original, name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _MistralChatProxy:
|
|
42
|
+
def __init__(self, chat) -> None:
|
|
43
|
+
self._chat = chat
|
|
44
|
+
self.complete = _MistralCompletionsProxy(chat).complete
|
|
45
|
+
|
|
46
|
+
def __getattr__(self, name: str):
|
|
47
|
+
return getattr(self._chat, name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MistralAdapter:
|
|
51
|
+
"""Wraps a mistralai.Mistral() client to intercept .chat.complete().
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
from mistralai import Mistral
|
|
55
|
+
from agentsnap.adapters.mistral import MistralAdapter
|
|
56
|
+
client = MistralAdapter(Mistral(api_key="..."))
|
|
57
|
+
response = client.chat.complete(model="mistral-large-latest", messages=[...])
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, client) -> None:
|
|
61
|
+
self._client = client
|
|
62
|
+
self.chat = _MistralChatProxy(client.chat)
|
|
63
|
+
|
|
64
|
+
def __getattr__(self, name: str):
|
|
65
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _CompletionsProxy:
|
|
7
|
+
def __init__(self, original) -> None:
|
|
8
|
+
self._original = original
|
|
9
|
+
|
|
10
|
+
def create(self, **kwargs):
|
|
11
|
+
acc = TraceAccumulator.current()
|
|
12
|
+
if acc is None:
|
|
13
|
+
return self._original.create(**kwargs)
|
|
14
|
+
|
|
15
|
+
messages = kwargs.get("messages", [])
|
|
16
|
+
# Force non-streaming so we always get a complete ChatCompletion object.
|
|
17
|
+
# Streaming responses expose deltas, not the full message content.
|
|
18
|
+
kwargs["stream"] = False
|
|
19
|
+
response = self._original.create(**kwargs)
|
|
20
|
+
|
|
21
|
+
response_text = ""
|
|
22
|
+
tokens = 0
|
|
23
|
+
if hasattr(response, "choices") and response.choices:
|
|
24
|
+
msg = response.choices[0].message
|
|
25
|
+
response_text = msg.content or ""
|
|
26
|
+
if hasattr(response, "usage"):
|
|
27
|
+
tokens = getattr(response.usage, "total_tokens", 0)
|
|
28
|
+
|
|
29
|
+
acc.push(
|
|
30
|
+
{
|
|
31
|
+
"type": "llm_call",
|
|
32
|
+
"messages": messages,
|
|
33
|
+
"response": response_text,
|
|
34
|
+
"tokens": tokens,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return response
|
|
38
|
+
|
|
39
|
+
def __getattr__(self, name: str):
|
|
40
|
+
return getattr(self._original, name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _ChatProxy:
|
|
44
|
+
def __init__(self, chat) -> None:
|
|
45
|
+
self._chat = chat
|
|
46
|
+
self.completions = _CompletionsProxy(chat.completions)
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, name: str):
|
|
49
|
+
return getattr(self._chat, name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OpenAIAdapter:
|
|
53
|
+
"""Wraps an openai.OpenAI() client to intercept .chat.completions.create()."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, client) -> None:
|
|
56
|
+
self._client = client
|
|
57
|
+
self.chat = _ChatProxy(client.chat)
|
|
58
|
+
|
|
59
|
+
def __getattr__(self, name: str):
|
|
60
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentsnap.adapters.openai import OpenAIAdapter
|
|
4
|
+
|
|
5
|
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenRouterAdapter(OpenAIAdapter):
|
|
9
|
+
"""Wraps an openai.OpenAI() client pointed at OpenRouter.
|
|
10
|
+
|
|
11
|
+
OpenRouter exposes an OpenAI-compatible API that proxies 300+ models
|
|
12
|
+
(Claude, GPT, Gemini, Llama, Mistral, etc.) under one key.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
import openai
|
|
16
|
+
from agentsnap.adapters.openrouter import OpenRouterAdapter, OPENROUTER_BASE_URL
|
|
17
|
+
|
|
18
|
+
client = OpenRouterAdapter(
|
|
19
|
+
openai.OpenAI(
|
|
20
|
+
api_key="sk-or-...",
|
|
21
|
+
base_url=OPENROUTER_BASE_URL,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
response = client.chat.completions.create(
|
|
25
|
+
model="anthropic/claude-haiku-4-5", # or any OpenRouter model slug
|
|
26
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from agentsnap.core.recorder import TraceAccumulator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolAdapter:
|
|
9
|
+
"""Wraps any callable to intercept and record tool calls."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, func: Callable, name: str | None = None) -> None:
|
|
12
|
+
self._func = func
|
|
13
|
+
self._name = name or getattr(func, "__name__", "unknown_tool")
|
|
14
|
+
|
|
15
|
+
def __call__(self, **kwargs: Any) -> Any:
|
|
16
|
+
acc = TraceAccumulator.current()
|
|
17
|
+
if acc is None:
|
|
18
|
+
return self._func(**kwargs)
|
|
19
|
+
|
|
20
|
+
result = self._func(**kwargs)
|
|
21
|
+
acc.push(
|
|
22
|
+
{
|
|
23
|
+
"type": "tool_call",
|
|
24
|
+
"name": self._name,
|
|
25
|
+
"args": kwargs,
|
|
26
|
+
"result": str(result),
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def __name__(self) -> str:
|
|
33
|
+
return self._name
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"ToolAdapter({self._name!r})"
|