memgraph-context-graph 0.5.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.
- memgraph_context_graph/__init__.py +71 -0
- memgraph_context_graph/adapters/__init__.py +0 -0
- memgraph_context_graph/adapters/_identity.py +520 -0
- memgraph_context_graph/adapters/_opencode_plugin.js +112 -0
- memgraph_context_graph/adapters/_spec.py +441 -0
- memgraph_context_graph/adapters/antigravity_cli.py +152 -0
- memgraph_context_graph/adapters/claude.py +297 -0
- memgraph_context_graph/adapters/claude_code.py +216 -0
- memgraph_context_graph/adapters/claude_code_memory.py +181 -0
- memgraph_context_graph/adapters/codex.py +118 -0
- memgraph_context_graph/adapters/codex_memory.py +185 -0
- memgraph_context_graph/adapters/copilot_cli.py +236 -0
- memgraph_context_graph/adapters/cursor.py +189 -0
- memgraph_context_graph/adapters/grok.py +187 -0
- memgraph_context_graph/adapters/openai.py +240 -0
- memgraph_context_graph/adapters/opencode.py +168 -0
- memgraph_context_graph/cli.py +845 -0
- memgraph_context_graph/events.py +210 -0
- memgraph_context_graph/hooks/__init__.py +1 -0
- memgraph_context_graph/hooks/cli.py +184 -0
- memgraph_context_graph/hooks/runner.py +311 -0
- memgraph_context_graph/hooks/runtime_plugin.py +85 -0
- memgraph_context_graph/link.py +50 -0
- memgraph_context_graph/mcp_server.py +102 -0
- memgraph_context_graph/memory_backend.py +144 -0
- memgraph_context_graph/protocols.py +67 -0
- memgraph_context_graph/spaces.py +214 -0
- memgraph_context_graph/tools.py +130 -0
- memgraph_context_graph-0.5.0.dist-info/METADATA +718 -0
- memgraph_context_graph-0.5.0.dist-info/RECORD +33 -0
- memgraph_context_graph-0.5.0.dist-info/WHEEL +4 -0
- memgraph_context_graph-0.5.0.dist-info/entry_points.txt +11 -0
- memgraph_context_graph-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""GitHub Copilot CLI command-hook runtime adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from memgraph_context_graph.adapters._spec import (
|
|
10
|
+
EventContext,
|
|
11
|
+
HookConfig,
|
|
12
|
+
RuntimeSpec,
|
|
13
|
+
SpecAdapter,
|
|
14
|
+
SpecPlugin,
|
|
15
|
+
extract_tool_result,
|
|
16
|
+
json_hook_installer,
|
|
17
|
+
session_start,
|
|
18
|
+
tool_start,
|
|
19
|
+
turn_end,
|
|
20
|
+
)
|
|
21
|
+
from memgraph_context_graph.events import (
|
|
22
|
+
AgentEndEvent,
|
|
23
|
+
AgentStartEvent,
|
|
24
|
+
ErrorOccurredEvent,
|
|
25
|
+
MessageEvent,
|
|
26
|
+
SessionEndEvent,
|
|
27
|
+
ToolEndEvent,
|
|
28
|
+
ToolStartEvent,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from memgraph_context_graph.events import Event
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _session_end(context: EventContext) -> SessionEndEvent:
|
|
36
|
+
reason = str(
|
|
37
|
+
context.payload.get("reason")
|
|
38
|
+
or context.payload.get("stop_reason")
|
|
39
|
+
or context.payload.get("stopReason")
|
|
40
|
+
or "completed"
|
|
41
|
+
)
|
|
42
|
+
return SessionEndEvent(**context.base(), status="completed" if reason in {"complete", "end_turn"} else reason)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _message(context: EventContext) -> MessageEvent:
|
|
46
|
+
return MessageEvent(**context.base(), role="user", content=context.value("prompt", ""))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _tool_input(context: EventContext) -> Any:
|
|
50
|
+
# Native Copilot payloads may carry toolArgs as a JSON-encoded string.
|
|
51
|
+
tool_input = context.value("tool_input")
|
|
52
|
+
if isinstance(tool_input, str):
|
|
53
|
+
try:
|
|
54
|
+
return json.loads(tool_input)
|
|
55
|
+
except ValueError:
|
|
56
|
+
return tool_input
|
|
57
|
+
return tool_input
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _tool_start(context: EventContext) -> ToolStartEvent:
|
|
61
|
+
return dataclasses.replace(tool_start(context), tool_input=_tool_input(context))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _tool_end(context: EventContext) -> ToolEndEvent:
|
|
65
|
+
tool_result = context.value("tool_result")
|
|
66
|
+
result, is_error, error_message = extract_tool_result(tool_result)
|
|
67
|
+
result_type = (
|
|
68
|
+
tool_result.get("resultType", tool_result.get("result_type")) if isinstance(tool_result, dict) else None
|
|
69
|
+
)
|
|
70
|
+
if result_type not in (None, "success"):
|
|
71
|
+
is_error = True
|
|
72
|
+
error_message = error_message or f"Copilot tool result: {result_type}"
|
|
73
|
+
return _with_tool_input(
|
|
74
|
+
context,
|
|
75
|
+
ToolEndEvent(
|
|
76
|
+
**context.base(),
|
|
77
|
+
tool_name=context.text("tool_name"),
|
|
78
|
+
tool_use_id=context.optional_text("tool_use_id"),
|
|
79
|
+
result=result,
|
|
80
|
+
is_error=is_error,
|
|
81
|
+
error_message=error_message,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _tool_failure(context: EventContext) -> ToolEndEvent:
|
|
87
|
+
return _with_tool_input(
|
|
88
|
+
context,
|
|
89
|
+
ToolEndEvent(
|
|
90
|
+
**context.base(),
|
|
91
|
+
tool_name=context.text("tool_name"),
|
|
92
|
+
tool_use_id=context.optional_text("tool_use_id"),
|
|
93
|
+
is_error=True,
|
|
94
|
+
error_message=str(context.payload.get("error") or "Tool failed"),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _with_tool_input(context: EventContext, event: ToolEndEvent) -> ToolEndEvent:
|
|
100
|
+
# Copilot sends no tool call id, so keep the input on the result to tell
|
|
101
|
+
# repeated calls of the same tool apart.
|
|
102
|
+
tool_input = _tool_input(context)
|
|
103
|
+
if tool_input is not None:
|
|
104
|
+
event.metadata["tool_input"] = tool_input
|
|
105
|
+
return event
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _permission(context: EventContext) -> MessageEvent:
|
|
109
|
+
return MessageEvent(**context.base(), role="system", content=context.text("tool_name", "permission_request"))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _agent_name(context: EventContext) -> str:
|
|
113
|
+
# subagentStart carries only agentName; subagentStop adds agentId and
|
|
114
|
+
# agentType. agentName is the one key both share, so it names the Agent.
|
|
115
|
+
return str(context.payload.get("agentName") or context.payload.get("agent_name") or context.text("agent_type"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _agent_start(context: EventContext) -> AgentStartEvent:
|
|
119
|
+
agent_name = _agent_name(context)
|
|
120
|
+
return AgentStartEvent(**context.base(), agent_name=agent_name, agent_type=agent_name)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _agent_end(context: EventContext) -> AgentEndEvent:
|
|
124
|
+
agent_name = _agent_name(context)
|
|
125
|
+
output = context.payload.get("last_assistant_message", context.payload.get("response"))
|
|
126
|
+
return AgentEndEvent(
|
|
127
|
+
**context.base(),
|
|
128
|
+
agent_name=agent_name,
|
|
129
|
+
agent_type=context.text("agent_type", agent_name),
|
|
130
|
+
output=None if output is None else str(output),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _turn_end(context: EventContext) -> list[Event]:
|
|
135
|
+
# agentStop ends one turn; sessionEnd ends the session. Its payload carries
|
|
136
|
+
# no reply text.
|
|
137
|
+
return turn_end(context, reason=context.payload.get("stopReason", context.payload.get("stop_reason")))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _error(context: EventContext) -> ErrorOccurredEvent:
|
|
141
|
+
error = context.payload.get("error")
|
|
142
|
+
error_dict = error if isinstance(error, dict) else {}
|
|
143
|
+
message = error_dict.get("message", error)
|
|
144
|
+
details = {"context": context.payload.get("error_context", context.payload.get("errorContext"))}
|
|
145
|
+
if error_dict.get("stack") is not None:
|
|
146
|
+
details["stack"] = error_dict["stack"]
|
|
147
|
+
return ErrorOccurredEvent(
|
|
148
|
+
**context.base(),
|
|
149
|
+
error_type=str(error_dict.get("name") or "copilot_error"),
|
|
150
|
+
error_message=str(message or "Copilot CLI error"),
|
|
151
|
+
error_details=details,
|
|
152
|
+
recoverable=bool(context.payload.get("recoverable", True)),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_HOOKS = (
|
|
157
|
+
"sessionStart",
|
|
158
|
+
"sessionEnd",
|
|
159
|
+
"userPromptSubmitted",
|
|
160
|
+
"preToolUse",
|
|
161
|
+
"postToolUse",
|
|
162
|
+
"postToolUseFailure",
|
|
163
|
+
"permissionRequest",
|
|
164
|
+
"subagentStart",
|
|
165
|
+
"subagentStop",
|
|
166
|
+
"agentStop",
|
|
167
|
+
"errorOccurred",
|
|
168
|
+
)
|
|
169
|
+
SPEC = RuntimeSpec(
|
|
170
|
+
name="copilot-cli",
|
|
171
|
+
source_sdk="copilot-cli",
|
|
172
|
+
event_key="hook_event_name",
|
|
173
|
+
hooks=_HOOKS,
|
|
174
|
+
rules={
|
|
175
|
+
"sessionStart": session_start,
|
|
176
|
+
"sessionEnd": _session_end,
|
|
177
|
+
"userPromptSubmitted": _message,
|
|
178
|
+
"preToolUse": _tool_start,
|
|
179
|
+
"postToolUse": _tool_end,
|
|
180
|
+
"postToolUseFailure": _tool_failure,
|
|
181
|
+
"permissionRequest": _permission,
|
|
182
|
+
"subagentStart": _agent_start,
|
|
183
|
+
"subagentStop": _agent_end,
|
|
184
|
+
"agentStop": _turn_end,
|
|
185
|
+
"errorOccurred": _error,
|
|
186
|
+
},
|
|
187
|
+
# VS Code-compatible hook files use PascalCase event names.
|
|
188
|
+
event_aliases={
|
|
189
|
+
**{hook[0].upper() + hook[1:]: hook for hook in _HOOKS},
|
|
190
|
+
"UserPromptSubmit": "userPromptSubmitted",
|
|
191
|
+
},
|
|
192
|
+
metadata_keys=(
|
|
193
|
+
"cwd",
|
|
194
|
+
"timestamp",
|
|
195
|
+
"source",
|
|
196
|
+
"transcript_path",
|
|
197
|
+
"reason",
|
|
198
|
+
"stop_reason",
|
|
199
|
+
"tool_name",
|
|
200
|
+
"tool_input",
|
|
201
|
+
"agent_id",
|
|
202
|
+
"agent_type",
|
|
203
|
+
"agent_name",
|
|
204
|
+
"agent_display_name",
|
|
205
|
+
"agent_description",
|
|
206
|
+
"agentName",
|
|
207
|
+
"agentDisplayName",
|
|
208
|
+
"agentDescription",
|
|
209
|
+
"error_context",
|
|
210
|
+
),
|
|
211
|
+
config=HookConfig(
|
|
212
|
+
path=".github/hooks/memgraph-context-graph.json",
|
|
213
|
+
layout="flat",
|
|
214
|
+
version=1,
|
|
215
|
+
merge=True,
|
|
216
|
+
timeout_key="timeoutSec",
|
|
217
|
+
inject_event_name=True,
|
|
218
|
+
matchers={
|
|
219
|
+
"preToolUse": ".*",
|
|
220
|
+
"postToolUse": ".*",
|
|
221
|
+
"permissionRequest": ".*",
|
|
222
|
+
"subagentStart": ".*",
|
|
223
|
+
},
|
|
224
|
+
),
|
|
225
|
+
probe_payload={"hook_event_name": "sessionEnd", "sessionId": "doctor", "reason": "complete"},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CopilotCLIHooksAdapter(SpecAdapter):
|
|
230
|
+
"""Convert Copilot CLI hook payloads into Event Protocol events."""
|
|
231
|
+
|
|
232
|
+
SPEC = SPEC
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
init = json_hook_installer(SPEC)
|
|
236
|
+
PLUGIN = SpecPlugin(SPEC, CopilotCLIHooksAdapter, init=init)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Cursor command-hook runtime adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from memgraph_context_graph.adapters._spec import (
|
|
8
|
+
EventContext,
|
|
9
|
+
FieldMap,
|
|
10
|
+
HookConfig,
|
|
11
|
+
RuntimeSpec,
|
|
12
|
+
SpecAdapter,
|
|
13
|
+
SpecPlugin,
|
|
14
|
+
event_user_id,
|
|
15
|
+
json_hook_installer,
|
|
16
|
+
tool_end,
|
|
17
|
+
tool_start,
|
|
18
|
+
turn_end,
|
|
19
|
+
)
|
|
20
|
+
from memgraph_context_graph.events import (
|
|
21
|
+
AgentEndEvent,
|
|
22
|
+
AgentStartEvent,
|
|
23
|
+
MessageEvent,
|
|
24
|
+
SessionEndEvent,
|
|
25
|
+
SessionStartEvent,
|
|
26
|
+
ToolEndEvent,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from memgraph_context_graph.events import Event
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _session_start(context: EventContext) -> SessionStartEvent:
|
|
34
|
+
workspace_roots = context.payload.get("workspace_roots")
|
|
35
|
+
cwd = workspace_roots[0] if isinstance(workspace_roots, list) and workspace_roots else context.value("cwd")
|
|
36
|
+
return SessionStartEvent(
|
|
37
|
+
**context.base(),
|
|
38
|
+
model=context.optional_text("model"),
|
|
39
|
+
working_directory=None if cwd is None else str(cwd),
|
|
40
|
+
user_id=event_user_id(context),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _session_end(context: EventContext) -> SessionEndEvent:
|
|
45
|
+
return SessionEndEvent(**context.base(), status=str(context.payload.get("reason") or "completed"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _user_message(context: EventContext) -> MessageEvent:
|
|
49
|
+
return MessageEvent(
|
|
50
|
+
**context.base(),
|
|
51
|
+
role="user",
|
|
52
|
+
content=context.value("prompt", ""),
|
|
53
|
+
model=context.optional_text("model"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _assistant_message(context: EventContext) -> MessageEvent:
|
|
58
|
+
return MessageEvent(
|
|
59
|
+
**context.base(),
|
|
60
|
+
role="assistant",
|
|
61
|
+
content=context.payload.get("text", ""),
|
|
62
|
+
model=context.optional_text("model"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _tool_failure(context: EventContext) -> ToolEndEvent:
|
|
67
|
+
return ToolEndEvent(
|
|
68
|
+
**context.base(),
|
|
69
|
+
tool_name=context.text("tool_name"),
|
|
70
|
+
tool_use_id=context.optional_text("tool_use_id"),
|
|
71
|
+
is_error=True,
|
|
72
|
+
error_message=str(context.payload.get("error_message") or "Cursor tool failed"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _agent_start(context: EventContext) -> AgentStartEvent:
|
|
77
|
+
agent_type = context.text("agent_type")
|
|
78
|
+
# Cursor's stop payload omits subagent_id. Use subagent_type as the common
|
|
79
|
+
# lifecycle key and preserve the start-only ID in metadata.
|
|
80
|
+
return AgentStartEvent(
|
|
81
|
+
**context.base(),
|
|
82
|
+
agent_name=agent_type,
|
|
83
|
+
agent_type=agent_type,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _agent_end(context: EventContext) -> AgentEndEvent:
|
|
88
|
+
agent_type = context.text("agent_type")
|
|
89
|
+
return AgentEndEvent(
|
|
90
|
+
**context.base(),
|
|
91
|
+
agent_name=agent_type,
|
|
92
|
+
agent_type=agent_type,
|
|
93
|
+
output=str(context.payload.get("summary")) if context.payload.get("summary") is not None else None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _turn_end(context: EventContext) -> list[Event]:
|
|
98
|
+
# stop ends one agent loop; sessionEnd ends the conversation.
|
|
99
|
+
return turn_end(context, reason=context.payload.get("status"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
SPEC = RuntimeSpec(
|
|
103
|
+
name="cursor",
|
|
104
|
+
source_sdk="cursor",
|
|
105
|
+
event_key="hook_event_name",
|
|
106
|
+
hooks=(
|
|
107
|
+
"sessionStart",
|
|
108
|
+
"sessionEnd",
|
|
109
|
+
"beforeSubmitPrompt",
|
|
110
|
+
"preToolUse",
|
|
111
|
+
"postToolUse",
|
|
112
|
+
"postToolUseFailure",
|
|
113
|
+
"subagentStart",
|
|
114
|
+
"subagentStop",
|
|
115
|
+
"afterAgentResponse",
|
|
116
|
+
"stop",
|
|
117
|
+
),
|
|
118
|
+
rules={
|
|
119
|
+
"sessionStart": _session_start,
|
|
120
|
+
"sessionEnd": _session_end,
|
|
121
|
+
"beforeSubmitPrompt": _user_message,
|
|
122
|
+
"preToolUse": tool_start,
|
|
123
|
+
"postToolUse": tool_end,
|
|
124
|
+
"postToolUseFailure": _tool_failure,
|
|
125
|
+
"subagentStart": _agent_start,
|
|
126
|
+
"subagentStop": _agent_end,
|
|
127
|
+
"afterAgentResponse": _assistant_message,
|
|
128
|
+
"stop": _turn_end,
|
|
129
|
+
},
|
|
130
|
+
metadata_keys=(
|
|
131
|
+
"generation_id",
|
|
132
|
+
"model_id",
|
|
133
|
+
"model_params",
|
|
134
|
+
"cursor_version",
|
|
135
|
+
"workspace_roots",
|
|
136
|
+
"user_email",
|
|
137
|
+
"transcript_path",
|
|
138
|
+
"tool_name",
|
|
139
|
+
"tool_input",
|
|
140
|
+
"tool_use_id",
|
|
141
|
+
"duration",
|
|
142
|
+
"failure_type",
|
|
143
|
+
"is_interrupt",
|
|
144
|
+
"task",
|
|
145
|
+
"subagent_id",
|
|
146
|
+
"subagent_type",
|
|
147
|
+
"description",
|
|
148
|
+
"status",
|
|
149
|
+
"duration_ms",
|
|
150
|
+
"agent_transcript_path",
|
|
151
|
+
),
|
|
152
|
+
config=HookConfig(
|
|
153
|
+
path=".cursor/hooks.json",
|
|
154
|
+
layout="flat",
|
|
155
|
+
version=1,
|
|
156
|
+
merge=True,
|
|
157
|
+
timeout_key="timeout",
|
|
158
|
+
matchers={
|
|
159
|
+
"preToolUse": "*",
|
|
160
|
+
"postToolUse": "*",
|
|
161
|
+
"postToolUseFailure": "*",
|
|
162
|
+
"subagentStart": "*",
|
|
163
|
+
"subagentStop": "*",
|
|
164
|
+
},
|
|
165
|
+
),
|
|
166
|
+
fields=FieldMap(session_id=("conversation_id", "session_id")),
|
|
167
|
+
# Cursor blocks a permission hook whose output doesn't match its schema, so
|
|
168
|
+
# these hooks must answer. "allow" is the weakest vote: any other hook's
|
|
169
|
+
# "deny" or "ask" still wins, so capture never loosens the user's policy.
|
|
170
|
+
responses={
|
|
171
|
+
"preToolUse": {"permission": "allow"},
|
|
172
|
+
"subagentStart": {"permission": "allow"},
|
|
173
|
+
"beforeSubmitPrompt": {"continue": True},
|
|
174
|
+
},
|
|
175
|
+
# Grok Build also runs .cursor/hooks.json hooks, with its own payloads
|
|
176
|
+
# (camelCase duplicates of every field); its grok runtime records those.
|
|
177
|
+
foreign_payload_keys=frozenset({"hookEventName"}),
|
|
178
|
+
probe_payload={"hook_event_name": "sessionEnd", "session_id": "doctor", "reason": "completed"},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CursorHooksAdapter(SpecAdapter):
|
|
183
|
+
"""Convert Cursor hook payloads into Event Protocol events."""
|
|
184
|
+
|
|
185
|
+
SPEC = SPEC
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
init = json_hook_installer(SPEC)
|
|
189
|
+
PLUGIN = SpecPlugin(SPEC, CursorHooksAdapter, init=init)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Grok Build (``grok``) command-hook runtime adapter.
|
|
2
|
+
|
|
3
|
+
Grok sends each payload field in both camelCase and Claude Code's snake_case,
|
|
4
|
+
so the shared field aliases resolve most of it; only its typed tool results
|
|
5
|
+
and per-turn ``Stop`` need Grok-specific rules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from memgraph_context_graph.adapters._spec import (
|
|
13
|
+
EventContext,
|
|
14
|
+
FieldMap,
|
|
15
|
+
HookConfig,
|
|
16
|
+
RuntimeSpec,
|
|
17
|
+
SpecAdapter,
|
|
18
|
+
SpecPlugin,
|
|
19
|
+
json_hook_installer,
|
|
20
|
+
session_start,
|
|
21
|
+
tool_start,
|
|
22
|
+
turn_end,
|
|
23
|
+
)
|
|
24
|
+
from memgraph_context_graph.events import (
|
|
25
|
+
AgentEndEvent,
|
|
26
|
+
AgentStartEvent,
|
|
27
|
+
ErrorOccurredEvent,
|
|
28
|
+
MessageEvent,
|
|
29
|
+
SessionEndEvent,
|
|
30
|
+
ToolEndEvent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from memgraph_context_graph.events import Event
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _user_prompt(context: EventContext) -> MessageEvent:
|
|
38
|
+
return MessageEvent(**context.base(), role="user", content=context.value("prompt", ""))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _tool_result(tool_response: Any) -> tuple[Any, bool]:
|
|
42
|
+
"""Return a typed Grok tool result's model-facing text and whether it failed.
|
|
43
|
+
|
|
44
|
+
Results are tagged by ``type``: shell results carry ``output_for_prompt``
|
|
45
|
+
and ``exit_code``, file reads carry ``FileContent.content``. Other types
|
|
46
|
+
are kept whole.
|
|
47
|
+
"""
|
|
48
|
+
if not isinstance(tool_response, dict):
|
|
49
|
+
return tool_response, False
|
|
50
|
+
exit_code = tool_response.get("exit_code")
|
|
51
|
+
text = tool_response.get("output_for_prompt")
|
|
52
|
+
if text is None:
|
|
53
|
+
file_content = tool_response.get("FileContent")
|
|
54
|
+
text = file_content.get("content") if isinstance(file_content, dict) else None
|
|
55
|
+
return (tool_response if text is None else text), exit_code not in (None, 0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _tool_end(context: EventContext) -> ToolEndEvent:
|
|
59
|
+
result, is_error = _tool_result(context.value("tool_result"))
|
|
60
|
+
return ToolEndEvent(
|
|
61
|
+
**context.base(),
|
|
62
|
+
tool_name=context.text("tool_name"),
|
|
63
|
+
tool_use_id=context.optional_text("tool_use_id"),
|
|
64
|
+
result=result,
|
|
65
|
+
is_error=is_error,
|
|
66
|
+
agent_name=context.optional_text("agent_id"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _tool_failure(context: EventContext) -> ToolEndEvent:
|
|
71
|
+
return ToolEndEvent(
|
|
72
|
+
**context.base(),
|
|
73
|
+
tool_name=context.text("tool_name"),
|
|
74
|
+
tool_use_id=context.optional_text("tool_use_id"),
|
|
75
|
+
is_error=True,
|
|
76
|
+
error_message=str(context.payload.get("error") or "Grok tool failed"),
|
|
77
|
+
agent_name=context.optional_text("agent_id"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _permission_denied(context: EventContext) -> ErrorOccurredEvent:
|
|
82
|
+
return ErrorOccurredEvent(
|
|
83
|
+
**context.base(),
|
|
84
|
+
error_type="permission_denied",
|
|
85
|
+
error_message=str(context.payload.get("reason") or context.text("tool_name", "Permission denied")),
|
|
86
|
+
recoverable=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _turn_end(context: EventContext) -> list[Event]:
|
|
91
|
+
# Stop fires at the end of every turn (reason "end_turn") and once more at
|
|
92
|
+
# shutdown, right after SessionEnd; only the former ends a turn.
|
|
93
|
+
reason = context.payload.get("reason")
|
|
94
|
+
if reason == "shutdown":
|
|
95
|
+
return []
|
|
96
|
+
reply = context.payload.get("lastAssistantMessage", context.payload.get("last_assistant_message"))
|
|
97
|
+
return turn_end(context, reply=reply, reason=reason)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _stop_failure(context: EventContext) -> ErrorOccurredEvent:
|
|
101
|
+
error = str(context.payload.get("error") or "unknown")
|
|
102
|
+
return ErrorOccurredEvent(
|
|
103
|
+
**context.base(),
|
|
104
|
+
error_type=error,
|
|
105
|
+
error_message=str(context.payload.get("reason") or error),
|
|
106
|
+
recoverable=True,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _session_end(context: EventContext) -> SessionEndEvent:
|
|
111
|
+
return SessionEndEvent(**context.base(), status="completed")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _agent_start(context: EventContext) -> AgentStartEvent:
|
|
115
|
+
agent_type = context.text("agent_type")
|
|
116
|
+
return AgentStartEvent(**context.base(), agent_name=context.text("agent_id", agent_type), agent_type=agent_type)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _agent_end(context: EventContext) -> AgentEndEvent:
|
|
120
|
+
agent_type = context.text("agent_type")
|
|
121
|
+
return AgentEndEvent(
|
|
122
|
+
**context.base(),
|
|
123
|
+
agent_name=context.text("agent_id", agent_type),
|
|
124
|
+
agent_type=agent_type,
|
|
125
|
+
output=context.payload.get("lastAssistantMessage", context.payload.get("last_assistant_message")),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
_HOOKS = (
|
|
130
|
+
"SessionStart",
|
|
131
|
+
"UserPromptSubmit",
|
|
132
|
+
"PreToolUse",
|
|
133
|
+
"PostToolUse",
|
|
134
|
+
"PostToolUseFailure",
|
|
135
|
+
"PermissionDenied",
|
|
136
|
+
"SubagentStart",
|
|
137
|
+
"SubagentStop",
|
|
138
|
+
"Stop",
|
|
139
|
+
"StopFailure",
|
|
140
|
+
"SessionEnd",
|
|
141
|
+
)
|
|
142
|
+
SPEC = RuntimeSpec(
|
|
143
|
+
name="grok",
|
|
144
|
+
source_sdk="grok",
|
|
145
|
+
event_key="hook_event_name",
|
|
146
|
+
hooks=_HOOKS,
|
|
147
|
+
rules={
|
|
148
|
+
"SessionStart": session_start,
|
|
149
|
+
"UserPromptSubmit": _user_prompt,
|
|
150
|
+
"PreToolUse": tool_start,
|
|
151
|
+
"PostToolUse": _tool_end,
|
|
152
|
+
"PostToolUseFailure": _tool_failure,
|
|
153
|
+
"PermissionDenied": _permission_denied,
|
|
154
|
+
"SubagentStart": _agent_start,
|
|
155
|
+
"SubagentStop": _agent_end,
|
|
156
|
+
"Stop": _turn_end,
|
|
157
|
+
"StopFailure": _stop_failure,
|
|
158
|
+
"SessionEnd": _session_end,
|
|
159
|
+
},
|
|
160
|
+
metadata_keys=(
|
|
161
|
+
"cwd",
|
|
162
|
+
"workspaceRoot",
|
|
163
|
+
"permissionMode",
|
|
164
|
+
"source",
|
|
165
|
+
"reason",
|
|
166
|
+
"promptId",
|
|
167
|
+
"transcriptPath",
|
|
168
|
+
"durationMs",
|
|
169
|
+
"isBackgrounded",
|
|
170
|
+
"toolInputTruncated",
|
|
171
|
+
"toolResultTruncated",
|
|
172
|
+
"stopHookActive",
|
|
173
|
+
),
|
|
174
|
+
config=HookConfig(path=".grok/hooks/memgraph-context-graph.json", layout="nested", merge=True),
|
|
175
|
+
fields=FieldMap(tool_result=("tool_response", "toolResult")),
|
|
176
|
+
probe_payload={"hook_event_name": "SessionEnd", "session_id": "doctor", "reason": "shutdown"},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class GrokHooksAdapter(SpecAdapter):
|
|
181
|
+
"""Convert Grok Build hook payloads into Event Protocol events."""
|
|
182
|
+
|
|
183
|
+
SPEC = SPEC
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
init = json_hook_installer(SPEC)
|
|
187
|
+
PLUGIN = SpecPlugin(SPEC, GrokHooksAdapter, init=init)
|