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.
- actions_graph/__init__.py +74 -0
- actions_graph/connector.py +273 -0
- actions_graph/core.py +1019 -0
- actions_graph/hooks.py +568 -0
- actions_graph/models.py +363 -0
- actions_graph-0.1.0.dist-info/METADATA +263 -0
- actions_graph-0.1.0.dist-info/RECORD +9 -0
- actions_graph-0.1.0.dist-info/WHEEL +4 -0
- actions_graph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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]
|