thoughtflow 0.0.1__py3-none-any.whl → 0.0.2__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.
- thoughtflow/__init__.py +54 -5
- thoughtflow/_util.py +108 -0
- thoughtflow/adapters/__init__.py +43 -0
- thoughtflow/adapters/anthropic.py +119 -0
- thoughtflow/adapters/base.py +140 -0
- thoughtflow/adapters/local.py +133 -0
- thoughtflow/adapters/openai.py +118 -0
- thoughtflow/agent.py +147 -0
- thoughtflow/eval/__init__.py +34 -0
- thoughtflow/eval/harness.py +200 -0
- thoughtflow/eval/replay.py +137 -0
- thoughtflow/memory/__init__.py +27 -0
- thoughtflow/memory/base.py +142 -0
- thoughtflow/message.py +140 -0
- thoughtflow/py.typed +2 -0
- thoughtflow/tools/__init__.py +27 -0
- thoughtflow/tools/base.py +145 -0
- thoughtflow/tools/registry.py +122 -0
- thoughtflow/trace/__init__.py +34 -0
- thoughtflow/trace/events.py +183 -0
- thoughtflow/trace/schema.py +111 -0
- thoughtflow/trace/session.py +141 -0
- thoughtflow-0.0.2.dist-info/METADATA +215 -0
- thoughtflow-0.0.2.dist-info/RECORD +26 -0
- {thoughtflow-0.0.1.dist-info → thoughtflow-0.0.2.dist-info}/WHEEL +1 -2
- {thoughtflow-0.0.1.dist-info → thoughtflow-0.0.2.dist-info/licenses}/LICENSE +1 -1
- thoughtflow/jtools1.py +0 -25
- thoughtflow/jtools2.py +0 -27
- thoughtflow-0.0.1.dist-info/METADATA +0 -17
- thoughtflow-0.0.1.dist-info/RECORD +0 -8
- thoughtflow-0.0.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base memory interface for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Memory hooks provide a clean pattern for memory integration:
|
|
5
|
+
- Memory retrieval produces context items
|
|
6
|
+
- Those items are explicitly inserted into the message list
|
|
7
|
+
- Memory writes are explicit events emitted by the agent run
|
|
8
|
+
|
|
9
|
+
This avoids:
|
|
10
|
+
- Hidden memory mutation
|
|
11
|
+
- "Where did this context come from?"
|
|
12
|
+
- Irreproducible behavior across runs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MemoryEvent:
|
|
25
|
+
"""An event representing a memory operation.
|
|
26
|
+
|
|
27
|
+
Captured in traces to maintain full visibility into memory interactions.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
event_type: Type of event (retrieve, store, delete).
|
|
31
|
+
timestamp: When the event occurred.
|
|
32
|
+
query: The retrieval query (for retrieve events).
|
|
33
|
+
content: The content being stored (for store events).
|
|
34
|
+
results: Retrieved memories (for retrieve events).
|
|
35
|
+
metadata: Additional event metadata.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
event_type: str # "retrieve", "store", "delete"
|
|
39
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
40
|
+
query: str | None = None
|
|
41
|
+
content: str | None = None
|
|
42
|
+
results: list[dict[str, Any]] = field(default_factory=list)
|
|
43
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]:
|
|
46
|
+
"""Convert to a serializable dict.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict representation of the event.
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"event_type": self.event_type,
|
|
53
|
+
"timestamp": self.timestamp.isoformat(),
|
|
54
|
+
"query": self.query,
|
|
55
|
+
"content": self.content,
|
|
56
|
+
"results": self.results,
|
|
57
|
+
"metadata": self.metadata,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MemoryHook(ABC):
|
|
62
|
+
"""Abstract base class for memory integrations.
|
|
63
|
+
|
|
64
|
+
Memory hooks allow agents to:
|
|
65
|
+
- Retrieve relevant context from long-term memory
|
|
66
|
+
- Store new information for future retrieval
|
|
67
|
+
- Maintain conversation history beyond context window
|
|
68
|
+
|
|
69
|
+
Implementations might include:
|
|
70
|
+
- Vector database (Pinecone, Weaviate, ChromaDB)
|
|
71
|
+
- Key-value store
|
|
72
|
+
- SQL database
|
|
73
|
+
- File-based storage
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> class SimpleMemory(MemoryHook):
|
|
77
|
+
... def __init__(self):
|
|
78
|
+
... self.memories = []
|
|
79
|
+
...
|
|
80
|
+
... def retrieve(self, query, k=5):
|
|
81
|
+
... # Simple keyword matching (real impl would use embeddings)
|
|
82
|
+
... matches = [m for m in self.memories if query.lower() in m["content"].lower()]
|
|
83
|
+
... return matches[:k]
|
|
84
|
+
...
|
|
85
|
+
... def store(self, content, metadata=None):
|
|
86
|
+
... self.memories.append({"content": content, "metadata": metadata or {}})
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def retrieve(
|
|
91
|
+
self,
|
|
92
|
+
query: str,
|
|
93
|
+
k: int = 5,
|
|
94
|
+
filters: dict[str, Any] | None = None,
|
|
95
|
+
) -> list[dict[str, Any]]:
|
|
96
|
+
"""Retrieve relevant memories for a query.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
query: The search query.
|
|
100
|
+
k: Maximum number of results to return.
|
|
101
|
+
filters: Optional filters to apply.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of memory dicts, each containing at least "content".
|
|
105
|
+
"""
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def store(
|
|
110
|
+
self,
|
|
111
|
+
content: str,
|
|
112
|
+
metadata: dict[str, Any] | None = None,
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Store a new memory.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
content: The content to store.
|
|
118
|
+
metadata: Optional metadata to associate with the memory.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
ID of the stored memory.
|
|
122
|
+
"""
|
|
123
|
+
raise NotImplementedError
|
|
124
|
+
|
|
125
|
+
def delete(self, memory_id: str) -> bool:
|
|
126
|
+
"""Delete a memory by ID.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
memory_id: ID of the memory to delete.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if deleted, False if not found.
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError("delete() not implemented for this memory hook")
|
|
135
|
+
|
|
136
|
+
def clear(self) -> int:
|
|
137
|
+
"""Clear all memories.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Number of memories deleted.
|
|
141
|
+
"""
|
|
142
|
+
raise NotImplementedError("clear() not implemented for this memory hook")
|
thoughtflow/message.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message schema for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Messages are the universal currency across providers. ThoughtFlow keeps
|
|
5
|
+
messages provider-agnostic, minimal, and stable.
|
|
6
|
+
|
|
7
|
+
Typical structure:
|
|
8
|
+
msg_list = [
|
|
9
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
10
|
+
{"role": "user", "content": "Hello!"},
|
|
11
|
+
]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Literal, TypeAlias
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Type aliases for clarity
|
|
21
|
+
Role: TypeAlias = Literal["system", "user", "assistant", "tool"]
|
|
22
|
+
MessageDict: TypeAlias = dict[str, Any]
|
|
23
|
+
MessageList: TypeAlias = list[MessageDict]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Message:
|
|
28
|
+
"""A single message in a conversation.
|
|
29
|
+
|
|
30
|
+
This is an optional structured representation. You can also use
|
|
31
|
+
plain dicts - ThoughtFlow accepts both.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
role: The role of the message sender (system, user, assistant, tool).
|
|
35
|
+
content: The text content of the message.
|
|
36
|
+
name: Optional name for the sender (useful for multi-agent scenarios).
|
|
37
|
+
tool_call_id: Optional ID linking to a tool call (for tool responses).
|
|
38
|
+
metadata: Optional metadata dict for extensions.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> msg = Message(role="user", content="Hello!")
|
|
42
|
+
>>> msg.to_dict()
|
|
43
|
+
{'role': 'user', 'content': 'Hello!'}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
role: Role
|
|
47
|
+
content: str
|
|
48
|
+
name: str | None = None
|
|
49
|
+
tool_call_id: str | None = None
|
|
50
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> MessageDict:
|
|
53
|
+
"""Convert to a provider-compatible dict.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict with role, content, and optional fields.
|
|
57
|
+
"""
|
|
58
|
+
result: MessageDict = {
|
|
59
|
+
"role": self.role,
|
|
60
|
+
"content": self.content,
|
|
61
|
+
}
|
|
62
|
+
if self.name:
|
|
63
|
+
result["name"] = self.name
|
|
64
|
+
if self.tool_call_id:
|
|
65
|
+
result["tool_call_id"] = self.tool_call_id
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: MessageDict) -> Message:
|
|
70
|
+
"""Create a Message from a dict.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
data: Dict with at least 'role' and 'content' keys.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A Message instance.
|
|
77
|
+
"""
|
|
78
|
+
return cls(
|
|
79
|
+
role=data["role"],
|
|
80
|
+
content=data["content"],
|
|
81
|
+
name=data.get("name"),
|
|
82
|
+
tool_call_id=data.get("tool_call_id"),
|
|
83
|
+
metadata=data.get("metadata", {}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def system(cls, content: str) -> Message:
|
|
88
|
+
"""Create a system message.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
content: The system prompt content.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A Message with role='system'.
|
|
95
|
+
"""
|
|
96
|
+
return cls(role="system", content=content)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def user(cls, content: str) -> Message:
|
|
100
|
+
"""Create a user message.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
content: The user's message content.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A Message with role='user'.
|
|
107
|
+
"""
|
|
108
|
+
return cls(role="user", content=content)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def assistant(cls, content: str) -> Message:
|
|
112
|
+
"""Create an assistant message.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
content: The assistant's response content.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A Message with role='assistant'.
|
|
119
|
+
"""
|
|
120
|
+
return cls(role="assistant", content=content)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def normalize_messages(messages: list[Message | MessageDict]) -> MessageList:
|
|
124
|
+
"""Normalize a list of messages to dicts.
|
|
125
|
+
|
|
126
|
+
Accepts both Message objects and dicts, returning a uniform list of dicts.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
messages: List of Message objects or dicts.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of message dicts.
|
|
133
|
+
"""
|
|
134
|
+
result: MessageList = []
|
|
135
|
+
for msg in messages:
|
|
136
|
+
if isinstance(msg, Message):
|
|
137
|
+
result.append(msg.to_dict())
|
|
138
|
+
else:
|
|
139
|
+
result.append(msg)
|
|
140
|
+
return result
|
thoughtflow/py.typed
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool interfaces for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Tools are functions with contracts that agents can invoke.
|
|
5
|
+
ThoughtFlow makes tool use explicit, testable, and auditable.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from thoughtflow.tools import Tool
|
|
9
|
+
>>>
|
|
10
|
+
>>> class Calculator(Tool):
|
|
11
|
+
... name = "calculator"
|
|
12
|
+
... description = "Perform arithmetic operations"
|
|
13
|
+
...
|
|
14
|
+
... def call(self, payload):
|
|
15
|
+
... return eval(payload["expression"])
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from thoughtflow.tools.base import Tool, ToolResult
|
|
21
|
+
from thoughtflow.tools.registry import ToolRegistry
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Tool",
|
|
25
|
+
"ToolResult",
|
|
26
|
+
"ToolRegistry",
|
|
27
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base tool interface for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Tools are functions with contracts. Tool invocation is an explicit step,
|
|
5
|
+
tool results are recorded in the trace, and tools can be simulated/stubbed
|
|
6
|
+
for deterministic tests.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolResult:
|
|
18
|
+
"""Result of a tool invocation.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
success: Whether the tool call succeeded.
|
|
22
|
+
output: The tool's output (if successful).
|
|
23
|
+
error: Error message (if failed).
|
|
24
|
+
metadata: Additional metadata about the call.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
success: bool
|
|
28
|
+
output: Any = None
|
|
29
|
+
error: str | None = None
|
|
30
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def ok(cls, output: Any, **metadata: Any) -> ToolResult:
|
|
34
|
+
"""Create a successful result.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
output: The tool's output.
|
|
38
|
+
**metadata: Additional metadata.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A successful ToolResult.
|
|
42
|
+
"""
|
|
43
|
+
return cls(success=True, output=output, metadata=metadata)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def fail(cls, error: str, **metadata: Any) -> ToolResult:
|
|
47
|
+
"""Create a failed result.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
error: Error message.
|
|
51
|
+
**metadata: Additional metadata.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A failed ToolResult.
|
|
55
|
+
"""
|
|
56
|
+
return cls(success=False, error=error, metadata=metadata)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Tool(ABC):
|
|
60
|
+
"""Abstract base class for tools.
|
|
61
|
+
|
|
62
|
+
Tools are the mechanism for agents to interact with the outside world.
|
|
63
|
+
Each tool has:
|
|
64
|
+
- A unique name
|
|
65
|
+
- A description (for the LLM to understand when to use it)
|
|
66
|
+
- A schema (JSON Schema for the expected input)
|
|
67
|
+
- A call method that executes the tool
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> class WebSearch(Tool):
|
|
71
|
+
... name = "web_search"
|
|
72
|
+
... description = "Search the web for information"
|
|
73
|
+
...
|
|
74
|
+
... def get_schema(self):
|
|
75
|
+
... return {
|
|
76
|
+
... "type": "object",
|
|
77
|
+
... "properties": {
|
|
78
|
+
... "query": {"type": "string"}
|
|
79
|
+
... },
|
|
80
|
+
... "required": ["query"]
|
|
81
|
+
... }
|
|
82
|
+
...
|
|
83
|
+
... def call(self, payload, params=None):
|
|
84
|
+
... query = payload["query"]
|
|
85
|
+
... # ... perform search ...
|
|
86
|
+
... return ToolResult.ok(results)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
# Subclasses should override these
|
|
90
|
+
name: str = "unnamed_tool"
|
|
91
|
+
description: str = "No description provided"
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def call(
|
|
95
|
+
self,
|
|
96
|
+
payload: dict[str, Any],
|
|
97
|
+
params: dict[str, Any] | None = None,
|
|
98
|
+
) -> ToolResult:
|
|
99
|
+
"""Execute the tool with the given payload.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
payload: The input data for the tool.
|
|
103
|
+
params: Optional execution parameters.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
ToolResult indicating success/failure and output.
|
|
107
|
+
"""
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
def get_schema(self) -> dict[str, Any]:
|
|
111
|
+
"""Get the JSON Schema for the tool's input.
|
|
112
|
+
|
|
113
|
+
Override this to provide a schema for the LLM.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
JSON Schema dict describing expected input.
|
|
117
|
+
"""
|
|
118
|
+
return {"type": "object", "properties": {}}
|
|
119
|
+
|
|
120
|
+
def to_openai_tool(self) -> dict[str, Any]:
|
|
121
|
+
"""Convert to OpenAI tool format.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dict in OpenAI's tool specification format.
|
|
125
|
+
"""
|
|
126
|
+
return {
|
|
127
|
+
"type": "function",
|
|
128
|
+
"function": {
|
|
129
|
+
"name": self.name,
|
|
130
|
+
"description": self.description,
|
|
131
|
+
"parameters": self.get_schema(),
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def to_anthropic_tool(self) -> dict[str, Any]:
|
|
136
|
+
"""Convert to Anthropic tool format.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dict in Anthropic's tool specification format.
|
|
140
|
+
"""
|
|
141
|
+
return {
|
|
142
|
+
"name": self.name,
|
|
143
|
+
"description": self.description,
|
|
144
|
+
"input_schema": self.get_schema(),
|
|
145
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool registry for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Provides an explicit registry for tools. This is optional - you can
|
|
5
|
+
also pass tools directly to agents. The registry is useful for
|
|
6
|
+
organizing and discovering available tools.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from thoughtflow.tools.base import Tool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolRegistry:
|
|
18
|
+
"""Registry for managing available tools.
|
|
19
|
+
|
|
20
|
+
The registry provides a central place to register and lookup tools.
|
|
21
|
+
This is completely optional - ThoughtFlow doesn't require using a registry.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> registry = ToolRegistry()
|
|
25
|
+
>>> registry.register(calculator_tool)
|
|
26
|
+
>>> registry.register(web_search_tool)
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Get a tool by name
|
|
29
|
+
>>> calc = registry.get("calculator")
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Get all tools
|
|
32
|
+
>>> all_tools = registry.list()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize an empty registry."""
|
|
37
|
+
self._tools: dict[str, Tool] = {}
|
|
38
|
+
|
|
39
|
+
def register(self, tool: Tool) -> None:
|
|
40
|
+
"""Register a tool.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
tool: The tool to register.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If a tool with the same name already exists.
|
|
47
|
+
"""
|
|
48
|
+
if tool.name in self._tools:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Tool '{tool.name}' is already registered. "
|
|
51
|
+
"Use replace=True to override."
|
|
52
|
+
)
|
|
53
|
+
self._tools[tool.name] = tool
|
|
54
|
+
|
|
55
|
+
def unregister(self, name: str) -> None:
|
|
56
|
+
"""Unregister a tool by name.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: Name of the tool to unregister.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
KeyError: If no tool with that name exists.
|
|
63
|
+
"""
|
|
64
|
+
if name not in self._tools:
|
|
65
|
+
raise KeyError(f"Tool '{name}' not found in registry")
|
|
66
|
+
del self._tools[name]
|
|
67
|
+
|
|
68
|
+
def get(self, name: str) -> Tool:
|
|
69
|
+
"""Get a tool by name.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Name of the tool.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The registered Tool.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
KeyError: If no tool with that name exists.
|
|
79
|
+
"""
|
|
80
|
+
if name not in self._tools:
|
|
81
|
+
raise KeyError(f"Tool '{name}' not found in registry")
|
|
82
|
+
return self._tools[name]
|
|
83
|
+
|
|
84
|
+
def list(self) -> list[Tool]:
|
|
85
|
+
"""List all registered tools.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of all registered tools.
|
|
89
|
+
"""
|
|
90
|
+
return list(self._tools.values())
|
|
91
|
+
|
|
92
|
+
def names(self) -> list[str]:
|
|
93
|
+
"""List all registered tool names.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of tool names.
|
|
97
|
+
"""
|
|
98
|
+
return list(self._tools.keys())
|
|
99
|
+
|
|
100
|
+
def to_openai_tools(self) -> list[dict]:
|
|
101
|
+
"""Convert all tools to OpenAI format.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of tool dicts in OpenAI format.
|
|
105
|
+
"""
|
|
106
|
+
return [tool.to_openai_tool() for tool in self._tools.values()]
|
|
107
|
+
|
|
108
|
+
def to_anthropic_tools(self) -> list[dict]:
|
|
109
|
+
"""Convert all tools to Anthropic format.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of tool dicts in Anthropic format.
|
|
113
|
+
"""
|
|
114
|
+
return [tool.to_anthropic_tool() for tool in self._tools.values()]
|
|
115
|
+
|
|
116
|
+
def __len__(self) -> int:
|
|
117
|
+
"""Return number of registered tools."""
|
|
118
|
+
return len(self._tools)
|
|
119
|
+
|
|
120
|
+
def __contains__(self, name: str) -> bool:
|
|
121
|
+
"""Check if a tool is registered."""
|
|
122
|
+
return name in self._tools
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tracing and session management for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Traces capture complete run state: inputs, outputs, tool calls, model calls,
|
|
5
|
+
timing, token usage, and costs. This enables debugging, evaluation,
|
|
6
|
+
reproducibility, regression testing, and replay/diff across versions.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from thoughtflow.trace import Session
|
|
10
|
+
>>>
|
|
11
|
+
>>> session = Session()
|
|
12
|
+
>>> response = agent.call(messages, session=session)
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Inspect the trace
|
|
15
|
+
>>> print(session.events)
|
|
16
|
+
>>> print(session.total_tokens)
|
|
17
|
+
>>> print(session.total_cost)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Save for replay
|
|
20
|
+
>>> session.save("trace.json")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from thoughtflow.trace.session import Session
|
|
26
|
+
from thoughtflow.trace.events import Event, EventType
|
|
27
|
+
from thoughtflow.trace.schema import TraceSchema
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Session",
|
|
31
|
+
"Event",
|
|
32
|
+
"EventType",
|
|
33
|
+
"TraceSchema",
|
|
34
|
+
]
|