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.
Files changed (33) hide show
  1. memgraph_context_graph/__init__.py +71 -0
  2. memgraph_context_graph/adapters/__init__.py +0 -0
  3. memgraph_context_graph/adapters/_identity.py +520 -0
  4. memgraph_context_graph/adapters/_opencode_plugin.js +112 -0
  5. memgraph_context_graph/adapters/_spec.py +441 -0
  6. memgraph_context_graph/adapters/antigravity_cli.py +152 -0
  7. memgraph_context_graph/adapters/claude.py +297 -0
  8. memgraph_context_graph/adapters/claude_code.py +216 -0
  9. memgraph_context_graph/adapters/claude_code_memory.py +181 -0
  10. memgraph_context_graph/adapters/codex.py +118 -0
  11. memgraph_context_graph/adapters/codex_memory.py +185 -0
  12. memgraph_context_graph/adapters/copilot_cli.py +236 -0
  13. memgraph_context_graph/adapters/cursor.py +189 -0
  14. memgraph_context_graph/adapters/grok.py +187 -0
  15. memgraph_context_graph/adapters/openai.py +240 -0
  16. memgraph_context_graph/adapters/opencode.py +168 -0
  17. memgraph_context_graph/cli.py +845 -0
  18. memgraph_context_graph/events.py +210 -0
  19. memgraph_context_graph/hooks/__init__.py +1 -0
  20. memgraph_context_graph/hooks/cli.py +184 -0
  21. memgraph_context_graph/hooks/runner.py +311 -0
  22. memgraph_context_graph/hooks/runtime_plugin.py +85 -0
  23. memgraph_context_graph/link.py +50 -0
  24. memgraph_context_graph/mcp_server.py +102 -0
  25. memgraph_context_graph/memory_backend.py +144 -0
  26. memgraph_context_graph/protocols.py +67 -0
  27. memgraph_context_graph/spaces.py +214 -0
  28. memgraph_context_graph/tools.py +130 -0
  29. memgraph_context_graph-0.5.0.dist-info/METADATA +718 -0
  30. memgraph_context_graph-0.5.0.dist-info/RECORD +33 -0
  31. memgraph_context_graph-0.5.0.dist-info/WHEEL +4 -0
  32. memgraph_context_graph-0.5.0.dist-info/entry_points.txt +11 -0
  33. memgraph_context_graph-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,240 @@
1
+ """OpenAI Agents SDK adapter for memgraph-context-graph.
2
+
3
+ Translates OpenAI Agents SDK lifecycle hooks (``RunHooksBase``) into the
4
+ common Event protocol and forwards them to the AgentLink hub.
5
+
6
+ Usage::
7
+
8
+ from memgraph_context_graph import AgentLink
9
+ from memgraph_context_graph.adapters.openai import OpenAIAdapter
10
+
11
+ link = AgentLink()
12
+ adapter = OpenAIAdapter(link, session_id="s-1")
13
+ hooks = adapter.get_runtime_hooks()
14
+
15
+ # Pass hooks to Runner
16
+ result = await Runner.run(agent, "Hello", hooks=hooks)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import contextlib
22
+ import json
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from memgraph_context_graph.events import (
26
+ AgentEndEvent,
27
+ AgentStartEvent,
28
+ HandoffEvent,
29
+ LLMEndEvent,
30
+ LLMStartEvent,
31
+ SessionEndEvent,
32
+ SessionStartEvent,
33
+ ToolEndEvent,
34
+ ToolStartEvent,
35
+ )
36
+ from memgraph_context_graph.protocols import RuntimeAdapter
37
+
38
+ if TYPE_CHECKING:
39
+ from memgraph_context_graph.link import AgentLink
40
+
41
+ _SOURCE = "openai"
42
+
43
+
44
+ class OpenAIAdapter(RuntimeAdapter):
45
+ """Adapter that converts OpenAI Agents SDK lifecycle hooks into memgraph-context-graph events.
46
+
47
+ Args:
48
+ link: The AgentLink hub to emit events to.
49
+ session_id: Session identifier for all events.
50
+ auto_session: If ``True``, emit SessionStartEvent on creation.
51
+ session_kwargs: Extra fields for SessionStartEvent (model, …).
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ link: AgentLink,
57
+ session_id: str,
58
+ *,
59
+ auto_session: bool = True,
60
+ session_kwargs: dict[str, Any] | None = None,
61
+ ) -> None:
62
+ self._link = link
63
+ self._session_id = session_id
64
+
65
+ if auto_session:
66
+ kw = session_kwargs or {}
67
+ self._link.emit(
68
+ SessionStartEvent(
69
+ session_id=session_id,
70
+ source_sdk=_SOURCE,
71
+ model=kw.get("model"),
72
+ working_directory=kw.get("working_directory"),
73
+ user_id=kw.get("user_id"),
74
+ metadata=kw.get("metadata", {}),
75
+ )
76
+ )
77
+
78
+ def get_runtime_hooks(self) -> Any:
79
+ """Return a ``RunHooksBase`` subclass for ``Runner.run(..., hooks=...)``."""
80
+ try:
81
+ from agents.lifecycle import RunHooksBase
82
+ except ImportError:
83
+ msg = "openai-agents is required for OpenAIAdapter. Install it with: pip install memgraph-context-graph[openai]"
84
+ raise ImportError(msg) # noqa: B904
85
+
86
+ adapter = self
87
+
88
+ class _Hooks(RunHooksBase): # type: ignore[type-arg]
89
+ async def on_agent_start(
90
+ self,
91
+ context: Any,
92
+ agent: Any,
93
+ ) -> None:
94
+ adapter._link.emit(
95
+ AgentStartEvent(
96
+ session_id=adapter._session_id,
97
+ source_sdk=_SOURCE,
98
+ agent_name=getattr(agent, "name", str(agent)),
99
+ agent_type=type(agent).__name__,
100
+ )
101
+ )
102
+
103
+ async def on_agent_end(
104
+ self,
105
+ context: Any,
106
+ agent: Any,
107
+ output: Any,
108
+ ) -> None:
109
+ adapter._link.emit(
110
+ AgentEndEvent(
111
+ session_id=adapter._session_id,
112
+ source_sdk=_SOURCE,
113
+ agent_name=getattr(agent, "name", str(agent)),
114
+ agent_type=type(agent).__name__,
115
+ output=str(output) if output else None,
116
+ )
117
+ )
118
+
119
+ async def on_tool_start(
120
+ self,
121
+ context: Any,
122
+ agent: Any,
123
+ tool: Any,
124
+ ) -> None:
125
+ tool_name = getattr(tool, "name", str(tool))
126
+ # ToolContext exposes tool_call_id and tool_arguments (JSON string)
127
+ tool_call_id = getattr(context, "tool_call_id", None)
128
+ tool_arguments = getattr(context, "tool_arguments", None)
129
+
130
+ # Parse JSON string to dict for downstream connectors
131
+ tool_input: Any = tool_arguments
132
+ if isinstance(tool_arguments, str):
133
+ with contextlib.suppress(json.JSONDecodeError):
134
+ tool_input = json.loads(tool_arguments)
135
+
136
+ adapter._link.emit(
137
+ ToolStartEvent(
138
+ session_id=adapter._session_id,
139
+ source_sdk=_SOURCE,
140
+ tool_name=tool_name,
141
+ tool_input=tool_input,
142
+ tool_use_id=tool_call_id,
143
+ agent_name=getattr(agent, "name", None),
144
+ )
145
+ )
146
+
147
+ async def on_tool_end(
148
+ self,
149
+ context: Any,
150
+ agent: Any,
151
+ tool: Any,
152
+ result: str,
153
+ ) -> None:
154
+ tool_name = getattr(tool, "name", str(tool))
155
+ tool_call_id = getattr(context, "tool_call_id", None)
156
+
157
+ adapter._link.emit(
158
+ ToolEndEvent(
159
+ session_id=adapter._session_id,
160
+ source_sdk=_SOURCE,
161
+ tool_name=tool_name,
162
+ tool_use_id=tool_call_id,
163
+ result=result,
164
+ agent_name=getattr(agent, "name", None),
165
+ )
166
+ )
167
+
168
+ async def on_handoff(
169
+ self,
170
+ context: Any,
171
+ from_agent: Any,
172
+ to_agent: Any,
173
+ ) -> None:
174
+ adapter._link.emit(
175
+ HandoffEvent(
176
+ session_id=adapter._session_id,
177
+ source_sdk=_SOURCE,
178
+ from_agent=getattr(from_agent, "name", str(from_agent)),
179
+ to_agent=getattr(to_agent, "name", str(to_agent)),
180
+ )
181
+ )
182
+
183
+ async def on_llm_start(
184
+ self,
185
+ context: Any,
186
+ agent: Any,
187
+ system_prompt: str | None,
188
+ input_items: list[Any],
189
+ ) -> None:
190
+ adapter._link.emit(
191
+ LLMStartEvent(
192
+ session_id=adapter._session_id,
193
+ source_sdk=_SOURCE,
194
+ agent_name=getattr(agent, "name", None),
195
+ system_prompt=system_prompt,
196
+ input_items=input_items,
197
+ )
198
+ )
199
+
200
+ async def on_llm_end(
201
+ self,
202
+ context: Any,
203
+ agent: Any,
204
+ response: Any,
205
+ ) -> None:
206
+ # Extract usage from response if available
207
+ usage = getattr(response, "usage", None)
208
+ adapter._link.emit(
209
+ LLMEndEvent(
210
+ session_id=adapter._session_id,
211
+ source_sdk=_SOURCE,
212
+ agent_name=getattr(agent, "name", None),
213
+ model=getattr(response, "model", None),
214
+ input_tokens=getattr(usage, "input_tokens", None) if usage else None,
215
+ output_tokens=getattr(usage, "output_tokens", None) if usage else None,
216
+ response=response,
217
+ )
218
+ )
219
+
220
+ return _Hooks()
221
+
222
+ def end_session(
223
+ self,
224
+ *,
225
+ status: str = "completed",
226
+ total_cost_usd: float | None = None,
227
+ total_input_tokens: int | None = None,
228
+ total_output_tokens: int | None = None,
229
+ ) -> None:
230
+ """Manually signal session end (call after Runner.run completes)."""
231
+ self._link.emit(
232
+ SessionEndEvent(
233
+ session_id=self._session_id,
234
+ source_sdk=_SOURCE,
235
+ status=status,
236
+ total_cost_usd=total_cost_usd,
237
+ total_input_tokens=total_input_tokens,
238
+ total_output_tokens=total_output_tokens,
239
+ )
240
+ )
@@ -0,0 +1,168 @@
1
+ """OpenCode V2 plugin runtime adapter and installer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shlex
7
+ from importlib import resources
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from memgraph_context_graph.adapters._spec import (
11
+ EventContext,
12
+ HookConfig,
13
+ RuntimeSpec,
14
+ SpecAdapter,
15
+ SpecPlugin,
16
+ hook_command_argv,
17
+ session_start,
18
+ tool_start,
19
+ turn_end,
20
+ )
21
+ from memgraph_context_graph.events import ErrorOccurredEvent, MessageEvent, SessionEndEvent, ToolEndEvent
22
+
23
+ if TYPE_CHECKING:
24
+ from pathlib import Path
25
+
26
+ from memgraph_context_graph.events import Event
27
+
28
+
29
+ def _session_end(context: EventContext) -> SessionEndEvent:
30
+ return SessionEndEvent(**context.base(), status="completed")
31
+
32
+
33
+ def _prompt(context: EventContext) -> MessageEvent:
34
+ return MessageEvent(**context.base(), role="user", content=context.value("prompt", ""))
35
+
36
+
37
+ def _assistant_text(context: EventContext) -> MessageEvent:
38
+ return MessageEvent(**context.base(), role="assistant", content=context.payload.get("text", ""))
39
+
40
+
41
+ def _tool_end(context: EventContext) -> ToolEndEvent:
42
+ error = context.payload.get("error")
43
+ error_dict = error if isinstance(error, dict) else {}
44
+ result, exit_code = _tool_result(context.value("tool_result"))
45
+ return ToolEndEvent(
46
+ **context.base(),
47
+ tool_name=context.text("tool_name"),
48
+ tool_use_id=context.optional_text("tool_use_id"),
49
+ result=result,
50
+ is_error=bool(context.payload.get("is_error")) or exit_code not in (None, 0),
51
+ error_message=str(error_dict.get("message", error)) if error else None,
52
+ )
53
+
54
+
55
+ def _tool_result(tool_result: Any) -> tuple[Any, Any]:
56
+ """Return a V2 tool result's model-facing text and shell exit code, if any.
57
+
58
+ V2 results are ``{output, content: [{type: "text", text}], metadata}``;
59
+ ``content`` is what the model saw, ``output`` is tool-specific.
60
+ """
61
+ if not isinstance(tool_result, dict):
62
+ return tool_result, None
63
+ output = tool_result.get("output")
64
+ exit_code = output.get("exit") if isinstance(output, dict) else None
65
+ parts = tool_result.get("content")
66
+ if isinstance(parts, list):
67
+ texts = [part["text"] for part in parts if isinstance(part, dict) and isinstance(part.get("text"), str)]
68
+ if texts:
69
+ return "\n".join(texts), exit_code
70
+ return tool_result, exit_code
71
+
72
+
73
+ def _execution_succeeded(context: EventContext) -> list[Event]:
74
+ # One prompt's execution finished; `opencode run` never deletes its
75
+ # session, so this is the only end signal it gives.
76
+ return turn_end(context)
77
+
78
+
79
+ def _execution_failed(context: EventContext) -> list[Event]:
80
+ error = context.payload.get("error")
81
+ error_dict = error if isinstance(error, dict) else {}
82
+ details = {"status": error_dict["status"]} if error_dict.get("status") is not None else {}
83
+ failure = ErrorOccurredEvent(
84
+ **context.base(),
85
+ error_type=str(error_dict.get("type") or "opencode_error"),
86
+ error_message=str(error_dict.get("message") or error or "OpenCode session execution failed"),
87
+ error_details=details,
88
+ recoverable=True,
89
+ )
90
+ return [failure, *turn_end(context)]
91
+
92
+
93
+ def _permission(context: EventContext) -> MessageEvent:
94
+ return MessageEvent(
95
+ **context.base(),
96
+ role="system",
97
+ content=context.payload.get("permission") or "permission_requested",
98
+ )
99
+
100
+
101
+ SPEC = RuntimeSpec(
102
+ name="opencode",
103
+ source_sdk="opencode",
104
+ event_key="hook_event_name",
105
+ # OpenCode has no JSON command hooks: the installed V2 plugin subscribes to
106
+ # its hooks and bus events and pipes each one, normalized, to
107
+ # ``hook run opencode`` (see _opencode_plugin.js for the field mapping).
108
+ hooks=(),
109
+ rules={
110
+ "session.created": session_start,
111
+ "session.deleted": _session_end,
112
+ "session.prompt": _prompt,
113
+ # Emitted once per completed assistant text part, unlike the streamed
114
+ # session.text.delta, so each assistant message is recorded once.
115
+ "session.text.ended": _assistant_text,
116
+ "tool.execute.before": tool_start,
117
+ "tool.execute.after": _tool_end,
118
+ "session.execution.succeeded": _execution_succeeded,
119
+ "session.execution.failed": _execution_failed,
120
+ "permission.asked": _permission,
121
+ },
122
+ metadata_keys=("cwd", "model", "metadata"),
123
+ config=HookConfig(layout="flat"),
124
+ probe_payload={"hook_event_name": "session.deleted", "session_id": "doctor"},
125
+ )
126
+
127
+
128
+ class OpenCodeHooksAdapter(SpecAdapter):
129
+ """Convert normalized OpenCode V2 plugin callbacks into events."""
130
+
131
+ SPEC = SPEC
132
+
133
+
134
+ _PLUGIN_PATH = ".opencode/plugins/memgraph-context-graph/index.js"
135
+
136
+
137
+ def init(
138
+ project_dir: Path,
139
+ connectors: list[str],
140
+ *,
141
+ hook_command: str | None = None,
142
+ timeout: int = 30,
143
+ force: bool = False,
144
+ ) -> None:
145
+ """Install the dependency-free OpenCode V2 capture plugin.
146
+
147
+ The hook command is embedded as an argv array and spawned directly, so no
148
+ shell (and no login-shell profile) sits between OpenCode and the hook.
149
+ *timeout* is accepted for CLI symmetry; OpenCode plugins have no hook timeout.
150
+
151
+ Raises:
152
+ FileExistsError: if the plugin file exists and *force* is false.
153
+ """
154
+ target = project_dir / _PLUGIN_PATH
155
+ if target.exists() and not force:
156
+ raise FileExistsError(f"Refusing to overwrite existing OpenCode plugin: {target} (pass force=True)")
157
+
158
+ argv = shlex.split(hook_command) if hook_command else hook_command_argv(SPEC.name, connectors)
159
+ template = (
160
+ resources.files("memgraph_context_graph.adapters").joinpath("_opencode_plugin.js").read_text(encoding="utf-8")
161
+ )
162
+ plugin = template.replace("__MEMGRAPH_CONTEXT_GRAPH_COMMAND__", json.dumps(argv))
163
+ target.parent.mkdir(parents=True, exist_ok=True)
164
+ target.write_text(plugin, encoding="utf-8")
165
+ print(f"Wrote {target}")
166
+
167
+
168
+ PLUGIN = SpecPlugin(SPEC, OpenCodeHooksAdapter, init=init)