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.
@@ -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,2 @@
1
+ # PEP 561 marker file
2
+ # This file indicates that the package supports type checking
@@ -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
+ ]