actions-graph 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.
@@ -0,0 +1,74 @@
1
+ """Actions Graph: Store and track LLM actions, tool calls, and sessions in Memgraph.
2
+
3
+ This package provides a graph-based storage system for tracking all LLM interactions,
4
+ including tool calls, messages, structured outputs, and session management.
5
+
6
+ Quick Start:
7
+ from actions_graph import ActionsGraph, Session, ToolCall
8
+
9
+ # Initialize the graph
10
+ graph = ActionsGraph()
11
+ graph.setup()
12
+
13
+ # Create a session
14
+ session = Session(session_id="my-session-123")
15
+ graph.create_session(session)
16
+
17
+ # Record tool calls
18
+ graph.record_tool_call(
19
+ session_id="my-session-123",
20
+ tool_name="Read",
21
+ tool_input={"file_path": "/path/to/file"},
22
+ )
23
+
24
+ Integration with Agent Context Graph:
25
+ from actions_graph import ActionsGraph
26
+ from actions_graph.connector import ActionsGraphConnector
27
+ from agent_context_graph import AgentLink
28
+
29
+ graph = ActionsGraph()
30
+ graph.setup()
31
+
32
+ link = AgentLink()
33
+ link.add_connector(ActionsGraphConnector(graph))
34
+ """
35
+
36
+ from .connector import ActionsGraphConnector
37
+ from .core import ActionsGraph
38
+ from .models import (
39
+ Action,
40
+ ActionStatus,
41
+ ActionType,
42
+ ActionValidationError,
43
+ ErrorEvent,
44
+ Message,
45
+ MessageRole,
46
+ PermissionRequest,
47
+ RateLimitEvent,
48
+ Session,
49
+ StructuredOutput,
50
+ SubagentEvent,
51
+ ToolCall,
52
+ ToolResult,
53
+ )
54
+
55
+ __all__ = [
56
+ "Action",
57
+ "ActionStatus",
58
+ "ActionType",
59
+ "ActionValidationError",
60
+ "ActionsGraph",
61
+ "ActionsGraphConnector",
62
+ "ErrorEvent",
63
+ "Message",
64
+ "MessageRole",
65
+ "PermissionRequest",
66
+ "RateLimitEvent",
67
+ "Session",
68
+ "StructuredOutput",
69
+ "SubagentEvent",
70
+ "ToolCall",
71
+ "ToolResult",
72
+ ]
73
+
74
+ __version__ = "0.1.0"
@@ -0,0 +1,273 @@
1
+ """Agent Context Graph connector for ActionsGraph.
2
+
3
+ The connector lives inside actions-graph because ActionsGraph owns the
4
+ interpretation of Event Protocol events as persisted action/session graph data.
5
+ Agent Context Graph only routes normalized runtime events.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from agent_context_graph.events import (
14
+ AgentEndEvent,
15
+ AgentStartEvent,
16
+ ErrorOccurredEvent,
17
+ Event,
18
+ EventType,
19
+ MessageEvent,
20
+ SessionEndEvent,
21
+ SessionStartEvent,
22
+ ToolEndEvent,
23
+ ToolStartEvent,
24
+ )
25
+ from agent_context_graph.protocols import GraphConnector
26
+
27
+ from .models import (
28
+ ActionStatus,
29
+ ActionType,
30
+ ErrorEvent,
31
+ Message,
32
+ MessageRole,
33
+ Session,
34
+ SubagentEvent,
35
+ ToolCall,
36
+ ToolResult,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from .core import ActionsGraph
41
+
42
+
43
+ _SUPPORTED_EVENTS = {
44
+ EventType.SESSION_START,
45
+ EventType.SESSION_END,
46
+ EventType.TOOL_START,
47
+ EventType.TOOL_END,
48
+ EventType.AGENT_START,
49
+ EventType.AGENT_END,
50
+ EventType.MESSAGE,
51
+ EventType.ERROR,
52
+ }
53
+
54
+
55
+ class ActionsGraphConnector(GraphConnector):
56
+ """Receives Event Protocol events and records session observability data."""
57
+
58
+ def __init__(self, graph: ActionsGraph) -> None:
59
+ self._graph = graph
60
+
61
+ def supports(self, event: Event) -> bool:
62
+ return event.event_type in _SUPPORTED_EVENTS
63
+
64
+ def on_event(self, event: Event) -> None:
65
+ if isinstance(event, SessionStartEvent):
66
+ self._on_session_start(event)
67
+ elif isinstance(event, SessionEndEvent):
68
+ self._on_session_end(event)
69
+ elif isinstance(event, ToolStartEvent):
70
+ self._on_tool_start(event)
71
+ elif isinstance(event, ToolEndEvent):
72
+ self._on_tool_end(event)
73
+ elif isinstance(event, AgentStartEvent):
74
+ self._on_agent_start(event)
75
+ elif isinstance(event, AgentEndEvent):
76
+ self._on_agent_end(event)
77
+ elif isinstance(event, MessageEvent):
78
+ self._on_message(event)
79
+ elif isinstance(event, ErrorOccurredEvent):
80
+ self._on_error(event)
81
+
82
+ def _on_session_start(self, event: SessionStartEvent) -> None:
83
+ if self._graph.get_session(event.session_id) is not None:
84
+ return
85
+ self._graph.create_session(
86
+ Session(
87
+ session_id=event.session_id,
88
+ started_at=event.timestamp,
89
+ model=event.model,
90
+ working_directory=event.working_directory,
91
+ tags=event.tags,
92
+ metadata=self._metadata(event),
93
+ )
94
+ )
95
+
96
+ def _on_session_end(self, event: SessionEndEvent) -> None:
97
+ self._ensure_session(event)
98
+ self._graph.end_session(
99
+ event.session_id,
100
+ status=self._status(event.status),
101
+ total_cost_usd=event.total_cost_usd,
102
+ total_input_tokens=event.total_input_tokens,
103
+ total_output_tokens=event.total_output_tokens,
104
+ )
105
+
106
+ def _on_tool_start(self, event: ToolStartEvent) -> None:
107
+ self._ensure_session(event)
108
+ self._graph.record_action(
109
+ ToolCall(
110
+ action_id=self._action_id(event),
111
+ session_id=event.session_id,
112
+ timestamp=event.timestamp,
113
+ status=ActionStatus.IN_PROGRESS,
114
+ tool_name=event.tool_name,
115
+ tool_input=self._dict_or_wrapped(event.tool_input),
116
+ tool_use_id=event.tool_use_id,
117
+ metadata=self._metadata(event, agent_name=event.agent_name),
118
+ )
119
+ )
120
+
121
+ def _on_tool_end(self, event: ToolEndEvent) -> None:
122
+ self._ensure_session(event)
123
+ self._graph.record_action(
124
+ ToolResult(
125
+ action_id=self._action_id(event),
126
+ session_id=event.session_id,
127
+ timestamp=event.timestamp,
128
+ status=ActionStatus.FAILED if event.is_error else ActionStatus.COMPLETED,
129
+ parent_action_id=self._tool_start_action_id(event) if event.tool_use_id else None,
130
+ tool_use_id=event.tool_use_id or "",
131
+ tool_name=event.tool_name,
132
+ content=self._content(event.result),
133
+ is_error=event.is_error,
134
+ error_message=event.error_message,
135
+ metadata=self._metadata(event, agent_name=event.agent_name),
136
+ )
137
+ )
138
+
139
+ def _on_agent_start(self, event: AgentStartEvent) -> None:
140
+ self._ensure_session(event)
141
+ action = SubagentEvent(
142
+ action_id=self._action_id(event),
143
+ session_id=event.session_id,
144
+ action_type=ActionType.SUBAGENT_START,
145
+ timestamp=event.timestamp,
146
+ agent_id=event.agent_name,
147
+ agent_type=event.agent_type,
148
+ description=self._metadata_value(event, "description", ""),
149
+ metadata=self._metadata(event, parent_agent_name=event.parent_agent_name),
150
+ )
151
+ self._graph.record_action(action)
152
+
153
+ def _on_agent_end(self, event: AgentEndEvent) -> None:
154
+ self._ensure_session(event)
155
+ action = SubagentEvent(
156
+ action_id=self._action_id(event),
157
+ session_id=event.session_id,
158
+ action_type=ActionType.SUBAGENT_STOP,
159
+ timestamp=event.timestamp,
160
+ agent_id=event.agent_name,
161
+ agent_type=event.agent_type,
162
+ result=self._content(event.output),
163
+ metadata=self._metadata(event),
164
+ )
165
+ self._graph.record_action(action)
166
+
167
+ def _on_message(self, event: MessageEvent) -> None:
168
+ self._ensure_session(event)
169
+ self._graph.record_action(
170
+ Message(
171
+ action_id=self._action_id(event),
172
+ session_id=event.session_id,
173
+ timestamp=event.timestamp,
174
+ role=self._role(event.role),
175
+ content=event.content,
176
+ model=event.model,
177
+ metadata=self._metadata(event),
178
+ )
179
+ )
180
+
181
+ def _on_error(self, event: ErrorOccurredEvent) -> None:
182
+ self._ensure_session(event)
183
+ self._graph.record_action(
184
+ ErrorEvent(
185
+ action_id=self._action_id(event),
186
+ session_id=event.session_id,
187
+ timestamp=event.timestamp,
188
+ error_type=event.error_type,
189
+ error_message=event.error_message,
190
+ error_details=event.error_details,
191
+ recoverable=event.recoverable,
192
+ metadata=self._metadata(event),
193
+ )
194
+ )
195
+
196
+ def _ensure_session(self, event: Event) -> None:
197
+ if self._graph.get_session(event.session_id) is not None:
198
+ return
199
+ ensure_session = getattr(self._graph, "ensure_session", None)
200
+ if ensure_session is not None:
201
+ ensure_session(
202
+ Session(
203
+ session_id=event.session_id,
204
+ started_at=event.timestamp,
205
+ metadata=self._metadata(event),
206
+ )
207
+ )
208
+ return
209
+ self._graph.create_session(
210
+ Session(
211
+ session_id=event.session_id,
212
+ started_at=event.timestamp,
213
+ metadata=self._metadata(event),
214
+ )
215
+ )
216
+
217
+ @staticmethod
218
+ def _metadata(event: Event, **extra: Any) -> dict[str, Any]:
219
+ metadata = dict(event.metadata)
220
+ if event.source_sdk:
221
+ metadata["source_sdk"] = event.source_sdk
222
+ for key, value in extra.items():
223
+ if value is not None:
224
+ metadata[key] = value
225
+ return metadata
226
+
227
+ @staticmethod
228
+ def _metadata_value(event: Event, key: str, default: str) -> str:
229
+ value = event.metadata.get(key)
230
+ return value if isinstance(value, str) else default
231
+
232
+ @staticmethod
233
+ def _status(value: str) -> ActionStatus:
234
+ try:
235
+ return ActionStatus(value)
236
+ except ValueError:
237
+ return ActionStatus.COMPLETED
238
+
239
+ @staticmethod
240
+ def _role(value: str) -> MessageRole:
241
+ try:
242
+ return MessageRole(value)
243
+ except ValueError:
244
+ return MessageRole.USER
245
+
246
+ @staticmethod
247
+ def _dict_or_wrapped(value: Any) -> dict[str, Any]:
248
+ if isinstance(value, dict):
249
+ return value
250
+ return {"value": value}
251
+
252
+ @staticmethod
253
+ def _content(value: Any) -> str | list[dict[str, Any]] | None:
254
+ if value is None or isinstance(value, str):
255
+ return value
256
+ if isinstance(value, list) and all(isinstance(item, dict) for item in value):
257
+ return value
258
+ return str(value)
259
+
260
+ @classmethod
261
+ def _action_id(cls, event: Event) -> str:
262
+ if isinstance(event, (ToolStartEvent, ToolEndEvent)) and event.tool_use_id:
263
+ return cls._stable_id(event.session_id, event.event_type.value, event.tool_use_id)
264
+ return cls._stable_id(event.session_id, event.event_type.value, event.timestamp)
265
+
266
+ @classmethod
267
+ def _tool_start_action_id(cls, event: ToolEndEvent) -> str:
268
+ return cls._stable_id(event.session_id, EventType.TOOL_START.value, event.tool_use_id or "")
269
+
270
+ @staticmethod
271
+ def _stable_id(*parts: str) -> str:
272
+ payload = "\0".join(parts)
273
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]