supermem 0.3.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 (84) hide show
  1. agent/__init__.py +15 -0
  2. agent/agent.py +203 -0
  3. agent/engine.py +332 -0
  4. agent/model.py +101 -0
  5. agent/pyproject.toml +34 -0
  6. agent/schemas.py +154 -0
  7. agent/settings.py +32 -0
  8. agent/system_prompt.txt +305 -0
  9. agent/tools.py +384 -0
  10. agent/utils.py +229 -0
  11. mcp_server/__init__.py +0 -0
  12. mcp_server/http_server.py +172 -0
  13. mcp_server/mcp_http_server.py +313 -0
  14. mcp_server/mcp_sse_server.py +330 -0
  15. mcp_server/pyproject.toml +31 -0
  16. mcp_server/scripts/filters.py +54 -0
  17. mcp_server/scripts/install_lms.sh +8 -0
  18. mcp_server/scripts/memory_setup.py +131 -0
  19. mcp_server/scripts/memory_setup_cli.py +116 -0
  20. mcp_server/scripts/setup_scripts_and_json.py +99 -0
  21. mcp_server/server.py +484 -0
  22. mcp_server/settings.py +8 -0
  23. memory_connectors/__init__.py +18 -0
  24. memory_connectors/base.py +105 -0
  25. memory_connectors/chatgpt_history/__init__.py +21 -0
  26. memory_connectors/chatgpt_history/connector.py +431 -0
  27. memory_connectors/chatgpt_history/converter.py +371 -0
  28. memory_connectors/chatgpt_history/embedding_connector.py +786 -0
  29. memory_connectors/chatgpt_history/parser.py +300 -0
  30. memory_connectors/chatgpt_history/types.py +81 -0
  31. memory_connectors/github_live/__init__.py +3 -0
  32. memory_connectors/github_live/connector.py +1093 -0
  33. memory_connectors/google_docs_live/__init__.py +3 -0
  34. memory_connectors/google_docs_live/connector.py +594 -0
  35. memory_connectors/memory_connect.py +657 -0
  36. memory_connectors/memory_wizard.py +593 -0
  37. memory_connectors/notion/__init__.py +5 -0
  38. memory_connectors/notion/connector.py +415 -0
  39. memory_connectors/notion/parser.py +321 -0
  40. memory_connectors/notion/types.py +132 -0
  41. memory_connectors/nuclino/__init__.py +5 -0
  42. memory_connectors/nuclino/connector.py +446 -0
  43. memory_connectors/nuclino/parser.py +456 -0
  44. memory_connectors/nuclino/types.py +106 -0
  45. supermem/__init__.py +3 -0
  46. supermem/__main__.py +232 -0
  47. supermem/capture/__init__.py +8 -0
  48. supermem/capture/compressor.py +99 -0
  49. supermem/capture/observation.py +72 -0
  50. supermem/capture/session.py +69 -0
  51. supermem/capture/timeline.py +37 -0
  52. supermem/config.py +109 -0
  53. supermem/core/__init__.py +14 -0
  54. supermem/core/connector.py +50 -0
  55. supermem/core/model_client.py +53 -0
  56. supermem/core/retriever.py +58 -0
  57. supermem/core/storage.py +34 -0
  58. supermem/errors.py +103 -0
  59. supermem/hooks/__init__.py +1 -0
  60. supermem/hooks/inject.py +121 -0
  61. supermem/hooks/learn.py +95 -0
  62. supermem/indexer/__init__.py +5 -0
  63. supermem/indexer/vault.py +197 -0
  64. supermem/logging.py +77 -0
  65. supermem/model/__init__.py +5 -0
  66. supermem/model/base.py +255 -0
  67. supermem/privacy/__init__.py +5 -0
  68. supermem/privacy/filter.py +41 -0
  69. supermem/pyproject.toml +38 -0
  70. supermem/retrieval/__init__.py +15 -0
  71. supermem/retrieval/agent.py +91 -0
  72. supermem/retrieval/fts.py +55 -0
  73. supermem/retrieval/graph.py +102 -0
  74. supermem/retrieval/hybrid.py +168 -0
  75. supermem/retrieval/vector.py +59 -0
  76. supermem/storage/__init__.py +7 -0
  77. supermem/storage/database.py +452 -0
  78. supermem/storage/graph.py +175 -0
  79. supermem/storage/vector.py +108 -0
  80. supermem-0.3.0.dist-info/METADATA +276 -0
  81. supermem-0.3.0.dist-info/RECORD +84 -0
  82. supermem-0.3.0.dist-info/WHEEL +4 -0
  83. supermem-0.3.0.dist-info/entry_points.txt +2 -0
  84. supermem-0.3.0.dist-info/licenses/LICENSE +202 -0
agent/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ Obsidian Agent package.
3
+ """
4
+
5
+ try:
6
+ from .agent import Agent
7
+ from .engine import execute_sandboxed_code
8
+
9
+ __all__ = [
10
+ "Agent",
11
+ "execute_sandboxed_code",
12
+ ]
13
+ except ImportError:
14
+ # If some modules can't be imported, just make the package importable
15
+ __all__ = []
agent/agent.py ADDED
@@ -0,0 +1,203 @@
1
+ import sys
2
+ from agent.engine import execute_sandboxed_code
3
+ from agent.model import get_model_response, create_openai_client, create_vllm_client
4
+ from agent.utils import (
5
+ load_system_prompt,
6
+ create_memory_if_not_exists,
7
+ extract_python_code,
8
+ format_results,
9
+ extract_reply,
10
+ extract_thoughts,
11
+ )
12
+ from agent.settings import (
13
+ MEMORY_PATH,
14
+ SAVE_CONVERSATION_PATH,
15
+ MAX_TOOL_TURNS,
16
+ VLLM_HOST,
17
+ VLLM_PORT,
18
+ OPENROUTER_STRONG_MODEL,
19
+ )
20
+ from agent.schemas import ChatMessage, Role, AgentResponse
21
+
22
+ from typing import Union, Tuple
23
+
24
+ import json
25
+ import os
26
+ import uuid
27
+
28
+
29
+ class Agent:
30
+ def __init__(
31
+ self,
32
+ max_tool_turns: int = MAX_TOOL_TURNS,
33
+ memory_path: str = None,
34
+ use_vllm: bool = False,
35
+ model: str = None,
36
+ predetermined_memory_path: bool = False,
37
+ ):
38
+ # Load the system prompt and add it to the conversation history
39
+ self.system_prompt = load_system_prompt()
40
+ self.messages: list[ChatMessage] = [
41
+ ChatMessage(role=Role.SYSTEM, content=self.system_prompt)
42
+ ]
43
+
44
+ # Set the maximum number of tool turns and use_vllm flag
45
+ self.max_tool_turns = max_tool_turns
46
+ self.use_vllm = use_vllm
47
+
48
+ # Set model: use provided model, or fallback to OPENROUTER_STRONG_MODEL
49
+ if model:
50
+ self.model = model
51
+ else:
52
+ self.model = OPENROUTER_STRONG_MODEL
53
+
54
+ # Each Agent instance gets its own clients to avoid bottlenecks
55
+ if use_vllm:
56
+ self._client = create_vllm_client(host=VLLM_HOST, port=VLLM_PORT)
57
+ else:
58
+ self._client = create_openai_client()
59
+
60
+ # Set memory_path: use provided path or fall back to default MEMORY_PATH
61
+ if memory_path is not None:
62
+ # Always place custom memory paths inside a "memory/" folder
63
+ if predetermined_memory_path:
64
+ self.memory_path = os.path.join("memory", memory_path)
65
+ else:
66
+ self.memory_path = memory_path
67
+ else:
68
+ # Use default MEMORY_PATH but also place it inside "memory/" folder
69
+ self.memory_path = os.path.join("memory", MEMORY_PATH)
70
+
71
+ # Ensure memory_path is absolute for consistency
72
+ self.memory_path = os.path.abspath(self.memory_path)
73
+
74
+ def _add_message(self, message: Union[ChatMessage, dict]):
75
+ """Add a message to the conversation history."""
76
+ if isinstance(message, dict):
77
+ self.messages.append(ChatMessage(**message))
78
+ elif isinstance(message, ChatMessage):
79
+ self.messages.append(message)
80
+ else:
81
+ raise ValueError("Invalid message type")
82
+
83
+ def extract_response_parts(self, response: str) -> Tuple[str, str, str]:
84
+ """
85
+ Extract the thoughts, reply and python code from the response.
86
+
87
+ Args:
88
+ response: The response from the agent.
89
+
90
+ Returns:
91
+ A tuple of the thoughts, reply and python code.
92
+ """
93
+ thoughts = extract_thoughts(response)
94
+ reply = extract_reply(response)
95
+ python_code = extract_python_code(response)
96
+
97
+ return thoughts, reply, python_code
98
+
99
+ def chat(self, message: str) -> AgentResponse:
100
+ """
101
+ Chat with the agent.
102
+
103
+ Args:
104
+ message: The message to chat with the agent.
105
+
106
+ Returns:
107
+ The response from the agent.
108
+ """
109
+ # Add the user message to the conversation history
110
+ self._add_message(ChatMessage(role=Role.USER, content=message))
111
+
112
+ # Get the response from the agent using this instance's clients
113
+ response = get_model_response(
114
+ messages=self.messages,
115
+ model=self.model, # Pass the model if specified
116
+ client=self._client,
117
+ use_vllm=self.use_vllm,
118
+ )
119
+
120
+ # Extract the thoughts, reply and python code from the response
121
+ thoughts, reply, python_code = self.extract_response_parts(response)
122
+
123
+ # Execute the code from the agent's response
124
+ result = ({}, "")
125
+ if python_code:
126
+ create_memory_if_not_exists(self.memory_path)
127
+ result = execute_sandboxed_code(
128
+ code=python_code,
129
+ allowed_path=self.memory_path,
130
+ import_module="agent.tools",
131
+ )
132
+
133
+ # Add the agent's response to the conversation history
134
+ self._add_message(ChatMessage(role=Role.ASSISTANT, content=response))
135
+
136
+ remaining_tool_turns = self.max_tool_turns
137
+ while remaining_tool_turns > 0 and not reply:
138
+ self._add_message(
139
+ ChatMessage(
140
+ role=Role.USER, content=format_results(result[0], result[1])
141
+ )
142
+ )
143
+ response = get_model_response(
144
+ messages=self.messages,
145
+ model=self.model, # Pass the model if specified
146
+ client=self._client,
147
+ use_vllm=self.use_vllm,
148
+ )
149
+
150
+ # Extract the thoughts, reply and python code from the response
151
+ thoughts, reply, python_code = self.extract_response_parts(response)
152
+
153
+ self._add_message(ChatMessage(role=Role.ASSISTANT, content=response))
154
+ if python_code:
155
+ create_memory_if_not_exists(self.memory_path)
156
+ result = execute_sandboxed_code(
157
+ code=python_code,
158
+ allowed_path=self.memory_path,
159
+ import_module="agent.tools",
160
+ )
161
+ else:
162
+ # Reset result when no Python code is executed
163
+ result = ({}, "")
164
+ remaining_tool_turns -= 1
165
+
166
+ return AgentResponse(thoughts=thoughts, reply=reply, python_block=python_code)
167
+
168
+ def save_conversation(self, log: bool = False, save_folder: str = None):
169
+ """
170
+ Save the conversation messages to a JSON file in
171
+ the output/conversations directory.
172
+ """
173
+ if not os.path.exists(SAVE_CONVERSATION_PATH):
174
+ os.makedirs(SAVE_CONVERSATION_PATH, exist_ok=True)
175
+
176
+ unique_id = uuid.uuid4()
177
+ if not save_folder:
178
+ file_path = os.path.join(SAVE_CONVERSATION_PATH, f"convo_{unique_id}.json")
179
+ else:
180
+ folder_path = (
181
+ save_folder # os.path.join(SAVE_CONVERSATION_PATH, save_folder)
182
+ )
183
+ if not os.path.exists(folder_path):
184
+ os.makedirs(folder_path)
185
+ file_path = os.path.join(folder_path, f"convo_{unique_id}.json")
186
+
187
+ # Convert the execution result messages to tool role
188
+ messages = [
189
+ (
190
+ ChatMessage(role=Role.TOOL, content=message.content)
191
+ if message.content.startswith("<result>")
192
+ else ChatMessage(role=message.role, content=message.content)
193
+ )
194
+ for message in self.messages
195
+ ]
196
+ try:
197
+ with open(file_path, "w") as f:
198
+ json.dump([message.model_dump() for message in messages], f, indent=4)
199
+ except Exception as e:
200
+ if log:
201
+ print(f"Error saving conversation: {e}")
202
+ if log:
203
+ print(f"Conversation saved to {file_path}")
agent/engine.py ADDED
@@ -0,0 +1,332 @@
1
+ import builtins
2
+ import importlib
3
+ import logging
4
+ import os
5
+ import sys
6
+ import traceback
7
+ import types
8
+ import pickle
9
+ import subprocess
10
+ import base64
11
+
12
+ from agent.settings import SANDBOX_TIMEOUT
13
+
14
+ # Configure a logger for the sandbox (in real use, configure handlers/level as needed)
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO) # or DEBUG for more verbosity
17
+
18
+
19
+ def _run_user_code(
20
+ code: str,
21
+ allow_installs: bool,
22
+ allowed_path: str,
23
+ blacklist: list,
24
+ available_functions: dict,
25
+ log: bool = False,
26
+ ) -> tuple[dict, str]:
27
+ """
28
+ Execute code under sandboxed conditions (limited file access, optional installs,
29
+ and blacklisting) and return the resulting locals and an error message.
30
+ """
31
+ try:
32
+ # Optional: apply working directory and file access restriction
33
+ if allowed_path:
34
+ allowed = os.path.abspath(allowed_path)
35
+ try:
36
+ os.chdir(allowed) # Change working dir to the allowed_path
37
+ except Exception as e:
38
+ # If we cannot chdir, log but continue (the open wrapper will still enforce path)
39
+ logger.warning(
40
+ "Could not change working directory to %s: %s", allowed, e
41
+ )
42
+ # Wrap builtins.open to restrict file access
43
+ orig_open = builtins.open
44
+
45
+ def secure_open(file, *args, **kwargs):
46
+ """Open that restricts file access to allowed_path."""
47
+ # If file is a file object or path-like, get its string path
48
+ path = (
49
+ file if isinstance(file, str) else getattr(file, "name", str(file))
50
+ )
51
+ full_path = os.path.abspath(path if path is not None else "")
52
+ if not full_path.startswith(allowed):
53
+ raise PermissionError(
54
+ f"Access to '{full_path}' is denied by sandbox."
55
+ )
56
+ return orig_open(file, *args, **kwargs)
57
+
58
+ builtins.open = secure_open
59
+
60
+ # Optionally, restrict other file-related functions (remove, rename, etc.) similarly
61
+ # We'll patch a couple of common ones as an example:
62
+ orig_remove = os.remove
63
+
64
+ def secure_remove(path, *args, **kwargs):
65
+ full_path = os.path.abspath(path)
66
+ if not full_path.startswith(allowed):
67
+ raise PermissionError(
68
+ f"Removal of '{full_path}' is denied by sandbox."
69
+ )
70
+ return orig_remove(path, *args, **kwargs)
71
+
72
+ os.remove = secure_remove
73
+
74
+ orig_rename = os.rename
75
+
76
+ def secure_rename(src, dst, *args, **kwargs):
77
+ full_src = os.path.abspath(src)
78
+ full_dst = os.path.abspath(dst)
79
+ if not full_src.startswith(allowed) or not full_dst.startswith(allowed):
80
+ raise PermissionError(
81
+ "Rename operation outside allowed path is denied by sandbox."
82
+ )
83
+ return orig_rename(src, dst, *args, **kwargs)
84
+
85
+ os.rename = secure_rename
86
+
87
+ # Apply blacklist restrictions by removing or disabling blacklisted builtins or attributes
88
+ if blacklist:
89
+ for name in blacklist:
90
+ # If the name has a dot, like "os.system", handle module attributes
91
+ if "." in name:
92
+ mod_name, attr_name = name.split(".", 1)
93
+ try:
94
+ mod_obj = importlib.import_module(mod_name)
95
+ except ImportError:
96
+ mod_obj = None
97
+ # If module is imported in sandbox, remove the attribute
98
+ if mod_obj and hasattr(mod_obj, attr_name):
99
+ try:
100
+ setattr(
101
+ mod_obj, attr_name, None
102
+ ) # simple way: nullify the attribute
103
+ except Exception:
104
+ pass # if we cannot set it, ignore (might be read-only)
105
+ else:
106
+ # It's a built-in or global name; remove from builtins if present
107
+ if name in builtins.__dict__:
108
+ builtins.__dict__[name] = (
109
+ None # or we could del, but setting None prevents use
110
+ )
111
+ # Additionally, we can ensure __builtins__ in the exec env doesn't contain them (handled below in exec)
112
+
113
+ # If allowed, handle package installations inside sandbox (in case code itself triggers ImportError)
114
+ if allow_installs:
115
+ # We will install missing imports on the fly during execution if an ImportError occurs.
116
+ # One approach: wrap __import__ to catch failed imports and pip install.
117
+ orig_import = builtins.__import__
118
+
119
+ def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
120
+ try:
121
+ return orig_import(name, globals, locals, fromlist, level)
122
+ except ImportError as e:
123
+ pkg = name.split(".")[0]
124
+ logger.info(
125
+ "Sandbox: attempting to install missing package '%s'", pkg
126
+ )
127
+ try:
128
+ subprocess.run(
129
+ [sys.executable, "-m", "pip", "install", pkg],
130
+ check=True,
131
+ stdout=subprocess.DEVNULL,
132
+ stderr=subprocess.DEVNULL,
133
+ )
134
+ except Exception as inst_err:
135
+ # If installation fails, re-raise the original ImportError
136
+ logger.error(
137
+ "Sandbox: failed to install package %s: %s", pkg, inst_err
138
+ )
139
+ raise e
140
+ # Retry the import after installation
141
+ return orig_import(name, globals, locals, fromlist, level)
142
+
143
+ builtins.__import__ = custom_import
144
+
145
+ # Prepare an isolated execution namespace. We use an empty globals dict with a fresh builtins.
146
+ exec_globals = {"__builtins__": builtins.__dict__}
147
+
148
+ # Add any provided functions to the execution environment
149
+ if available_functions:
150
+ exec_globals.update(available_functions)
151
+
152
+ exec_locals = {} # local variables will be collected here
153
+
154
+ error_msg = None
155
+ try:
156
+ exec(code, exec_globals, exec_locals) # Execute the user's code
157
+ except Exception as e:
158
+ # Catch any exception and format it
159
+ tb = traceback.format_exc()
160
+ error_msg = f"Exception in sandboxed code:\n{tb}"
161
+ if log:
162
+ logger.error("Sandbox: code raised an exception: %s", e)
163
+ except SystemExit as e:
164
+ # Handle sys.exit calls (which raise SystemExit)
165
+ code_val = e.code if isinstance(e.code, int) or e.code else 0
166
+ if code_val != 0:
167
+ error_msg = f"Sandboxed code called sys.exit({code_val})"
168
+ if log:
169
+ logger.warning(
170
+ "Sandbox: code exited with non-zero status %s", code_val
171
+ )
172
+ # For sys.exit(0), we treat it as normal termination (no error)
173
+
174
+ # Clean up any blacklisted or internal entries in locals
175
+ exec_locals.pop("__builtins__", None)
176
+
177
+ # Collect only picklable locals for returning
178
+ safe_locals = {}
179
+ for var, val in exec_locals.items():
180
+ try:
181
+ pickle.dumps(val) # test picklability
182
+ safe_locals[var] = val
183
+ except Exception:
184
+ safe_locals[var] = repr(val) # fallback: use string representation
185
+
186
+ if log:
187
+ logger.info("Sandbox execution finished")
188
+
189
+ return safe_locals, error_msg
190
+
191
+ except Exception as e:
192
+ # Catch any unhandled exceptions in the worker process
193
+ if log:
194
+ logger.error(
195
+ "Unhandled exception in sandbox worker: %s", traceback.format_exc()
196
+ )
197
+ return None, f"Sandbox worker error: {str(e)}"
198
+
199
+
200
+ def execute_sandboxed_code(
201
+ code: str,
202
+ timeout: int = SANDBOX_TIMEOUT,
203
+ allow_installs: bool = False,
204
+ requirements_path: str = None,
205
+ allowed_path: str = None,
206
+ blacklist: list = None,
207
+ available_functions: dict = None,
208
+ import_module: str = None,
209
+ log: bool = False,
210
+ ) -> tuple[dict, str]:
211
+ """
212
+ Execute the given Python code string in a sandboxed subprocess with specified restrictions.
213
+
214
+ Parameters:
215
+ code (str): The Python code to execute.
216
+ timeout (int): Maximum execution time in seconds for the sandboxed code (default 10 seconds).
217
+ allow_installs (bool): If True, allow installing missing packages via pip (default False).
218
+ requirements_path (str): Path to a requirements.txt file to install before execution.
219
+ allowed_path (str): Directory path that the code is allowed to access for file I/O.
220
+ File operations outside this path will be blocked. If None, no extra file restrictions are applied.
221
+ blacklist (list): List of names (builtins or module attributes) that are disallowed in the code.
222
+ If the code uses any of these, it will be prevented or result in an error.
223
+ available_functions (dict): Dictionary of functions to make available in the sandboxed environment.
224
+ The keys are the function names, and the values are the function objects.
225
+ import_module (str): Name of a Python module to import and make all its functions available in the sandbox.
226
+
227
+ Returns:
228
+ (dict, str): A tuple containing the dictionary of local variables from the executed code (or None on failure),
229
+ and an error message (str) if an error/exception occurred, or None if execution was successful.
230
+ """
231
+ # Step 1: If package installs are allowed, handle requirements and prepare environment
232
+ if requirements_path:
233
+ if os.path.isfile(requirements_path):
234
+ logger.info(
235
+ "Installing packages from requirements file: %s", requirements_path
236
+ )
237
+ try:
238
+ subprocess.run(
239
+ [sys.executable, "-m", "pip", "install", "-r", requirements_path],
240
+ check=True,
241
+ stdout=subprocess.DEVNULL,
242
+ stderr=subprocess.DEVNULL,
243
+ )
244
+ except Exception as e:
245
+ logger.error(
246
+ "Failed to install requirements from %s: %s", requirements_path, e
247
+ )
248
+ # If requirements fail to install, we can choose to abort or continue. Here, abort execution.
249
+ return None, f"Failed to install requirements: {e}"
250
+ else:
251
+ logger.error("Requirements file %s not found.", requirements_path)
252
+ return None, f"Requirements file not found: {requirements_path}"
253
+
254
+ # If a module name is provided, import it and add its functions to available_functions
255
+ if isinstance(available_functions, str) and not import_module:
256
+ import_module = available_functions
257
+ available_functions = None
258
+
259
+ if import_module:
260
+ try:
261
+ module = importlib.import_module(import_module)
262
+ if available_functions is None:
263
+ available_functions = {}
264
+ for name in dir(module):
265
+ if not name.startswith("_"):
266
+ attr = getattr(module, name)
267
+ if callable(attr):
268
+ available_functions[name] = attr
269
+ except ImportError as e:
270
+ logger.error(f"Failed to import module {import_module}: {e}")
271
+ return None, f"Failed to import module {import_module}: {e}"
272
+
273
+ # Step 2: Execute the code in a separate Python subprocess
274
+ params = {
275
+ "code": code,
276
+ "allow_installs": allow_installs,
277
+ "allowed_path": allowed_path,
278
+ "blacklist": blacklist or [],
279
+ "available_functions": available_functions or {},
280
+ "log": log,
281
+ }
282
+
283
+ env = os.environ.copy()
284
+ env["SANDBOX_PARAMS"] = base64.b64encode(pickle.dumps(params)).decode()
285
+
286
+ try:
287
+ result = subprocess.run(
288
+ [sys.executable, "-m", "agent.engine"],
289
+ stdout=subprocess.PIPE,
290
+ stderr=subprocess.PIPE,
291
+ timeout=timeout,
292
+ env=env,
293
+ )
294
+ except subprocess.TimeoutExpired:
295
+ logger.error(
296
+ "Sandboxed code exceeded time limit of %d seconds; terminating.", timeout
297
+ )
298
+ return None, f"TimeoutError: Code execution exceeded {timeout} seconds."
299
+
300
+ if result.returncode != 0:
301
+ return None, result.stderr.decode().strip()
302
+
303
+ try:
304
+ local_vars, error_msg = pickle.loads(result.stdout)
305
+ except Exception as e:
306
+ return None, f"Failed to decode sandbox output: {e}"
307
+
308
+ if error_msg is None:
309
+ error_msg = ""
310
+
311
+ return local_vars, error_msg
312
+
313
+
314
+ def _subprocess_entry() -> None:
315
+ """Entry point for sandbox subprocess."""
316
+ params_b64 = os.environ.get("SANDBOX_PARAMS")
317
+ if not params_b64:
318
+ sys.exit(1)
319
+ params = pickle.loads(base64.b64decode(params_b64))
320
+ locals_dict, error = _run_user_code(
321
+ params["code"],
322
+ params.get("allow_installs", False),
323
+ params.get("allowed_path"),
324
+ params.get("blacklist", []),
325
+ params.get("available_functions", {}),
326
+ params.get("log", False),
327
+ )
328
+ sys.stdout.buffer.write(pickle.dumps((locals_dict, error)))
329
+
330
+
331
+ if __name__ == "__main__":
332
+ _subprocess_entry()
agent/model.py ADDED
@@ -0,0 +1,101 @@
1
+ from openai import OpenAI
2
+ from pydantic import BaseModel
3
+
4
+ from typing import Optional, Union
5
+
6
+ from agent.settings import (
7
+ OPENROUTER_API_KEY,
8
+ OPENROUTER_BASE_URL,
9
+ OPENROUTER_STRONG_MODEL,
10
+ )
11
+ from agent.schemas import ChatMessage, Role
12
+
13
+
14
+ def create_openai_client() -> OpenAI:
15
+ """Create a new OpenAI client instance."""
16
+ return OpenAI(
17
+ api_key=OPENROUTER_API_KEY,
18
+ base_url=OPENROUTER_BASE_URL,
19
+ )
20
+
21
+
22
+ def create_vllm_client(host: str = "0.0.0.0", port: int = 8000) -> OpenAI:
23
+ """Create a new vLLM client instance (OpenAI-compatible)."""
24
+ return OpenAI(
25
+ base_url=f"http://{host}:{port}/v1",
26
+ api_key="EMPTY",
27
+ )
28
+
29
+
30
+ def _as_dict(msg: Union[ChatMessage, dict]) -> dict:
31
+ """
32
+ Accept either ChatMessage or raw dict and return the raw dict.
33
+
34
+ Args:
35
+ msg: A ChatMessage object or a raw dict.
36
+
37
+ Returns:
38
+ A raw dict.
39
+ """
40
+ return msg if isinstance(msg, dict) else msg.model_dump()
41
+
42
+
43
+ def get_model_response(
44
+ messages: Optional[list[ChatMessage]] = None,
45
+ message: Optional[str] = None,
46
+ system_prompt: Optional[str] = None,
47
+ model: str = OPENROUTER_STRONG_MODEL,
48
+ client: Optional[OpenAI] = None,
49
+ use_vllm: bool = False,
50
+ ) -> Union[str, BaseModel]:
51
+ """
52
+ Get a response from a model using OpenRouter or vLLM, with optional schema for structured output.
53
+
54
+ Args:
55
+ messages: A list of ChatMessage objects (optional).
56
+ message: A single message string (optional).
57
+ system_prompt: A system prompt for the model (optional).
58
+ model: The model to use.
59
+ schema: A Pydantic BaseModel for structured output (optional).
60
+ client: Optional OpenAI client to use. If None, uses the global client.
61
+ use_vllm: Whether to use vLLM backend instead of OpenRouter.
62
+
63
+ Returns:
64
+ A string response from the model if schema is None, otherwise a BaseModel object.
65
+ """
66
+ if messages is None and message is None:
67
+ raise ValueError("Either 'messages' or 'message' must be provided.")
68
+
69
+ # Use provided clients or fall back to global ones
70
+ if client is None:
71
+ if use_vllm:
72
+ client = create_vllm_client()
73
+ else:
74
+ client = create_openai_client()
75
+
76
+ # Build message history
77
+ if messages is None:
78
+ messages = []
79
+ if system_prompt:
80
+ messages.append(
81
+ _as_dict(ChatMessage(role=Role.SYSTEM, content=system_prompt))
82
+ )
83
+ messages.append(_as_dict(ChatMessage(role=Role.USER, content=message)))
84
+ else:
85
+ messages = [_as_dict(m) for m in messages]
86
+
87
+ if use_vllm:
88
+ completion = client.chat.completions.create(
89
+ model=model,
90
+ messages=messages,
91
+ # stop=["</reply>", "</python>"]
92
+ )
93
+
94
+ return completion.choices[0].message.content
95
+ else:
96
+ completion = client.chat.completions.create(
97
+ model=model,
98
+ messages=messages,
99
+ # stop=["</reply>", "</python>"]
100
+ )
101
+ return completion.choices[0].message.content
agent/pyproject.toml ADDED
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "agent"
3
+ version = "0.1.0"
4
+ description = "Agent module for mem-agent-mcp project"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "openai",
8
+ "pydantic>=2.0.0",
9
+ "python-dotenv",
10
+ "aiofiles",
11
+ "jinja2",
12
+ "black",
13
+ "huggingface-hub>=0.34.3",
14
+ "transformers>4.51.0,<4.54.0",
15
+ "vllm>=0.5.5; sys_platform != 'darwin'",
16
+ "setuptools"
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["setuptools>=61.0", "wheel"]
21
+ build-backend = "setuptools.build_meta"
22
+
23
+ [tool.setuptools]
24
+ py-modules = ["agent", "tools", "model", "engine", "schemas", "utils", "settings"]
25
+
26
+ [tool.setuptools.package-data]
27
+ "*" = ["*.txt"]
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=7.0.0",
32
+ "pytest-asyncio>=0.23.0",
33
+ "pytest-cov>=4.0.0",
34
+ ]