A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Added /start and modified /completion api

- /start - Will load the model to memory and prepare the internal agent
- /completions - Will now execute the py fns and update the memory

madclaws 64bff5eb 286e2d19

+1126 -219
+103 -217
server/api.py
··· 21 21 # SOFTWARE. 22 22 23 23 from fastapi import FastAPI, HTTPException 24 - from .config import SYSTEM_PROMPT 24 + from .config import SYSTEM_PROMPT,MEMORY_PATH 25 25 26 26 import json 27 27 import time ··· 38 38 ) 39 39 from .mlx_runner import MLXRunner 40 40 41 + from server.mem_agent.utils import extract_python_code, extract_reply, extract_thoughts, create_memory_if_not_exists, format_results 42 + from server.mem_agent.engine import execute_sandboxed_code 41 43 # Global model cache and configuration 42 44 _model_cache: Dict[str, MLXRunner] = {} 43 45 _current_model_path: Optional[str] = None 44 46 _default_max_tokens: Optional[int] = None # Use dynamic model-aware limits by default 45 - 47 + _runner: MLXRunner = {} 48 + _max_tool_turns = 5 46 49 47 50 class CompletionRequest(BaseModel): 48 51 model: str ··· 59 62 role: str = Field(..., pattern="^(system|user|assistant)$") 60 63 content: str 61 64 65 + _messages: list[ChatMessage]= [] 62 66 63 67 class ChatCompletionRequest(BaseModel): 64 68 model: str ··· 86 90 created: int 87 91 model: str 88 92 choices: List[Dict[str, Any]] 89 - usage: Dict[str, int] 93 + # usage: Dict[str, int] 90 94 91 95 92 96 class ModelInfo(BaseModel): ··· 96 100 permission: List = [] 97 101 context_length: Optional[int] = None 98 102 103 + class StartRequest(BaseModel): 104 + model: str 99 105 106 + class Agent: 107 + def __init__( 108 + self, 109 + max_tool_turns: int = 20, 110 + memory_path: str = None, 111 + use_vllm: bool = False, 112 + model: str = None, 113 + predetermined_memory_path: bool = False, 114 + model_cache: Dict[str, MLXRunner] = {}, 115 + current_model_path: Optional[str] = None, 116 + default_max_tokens: Optional[int] = None # Use dynamic model-aware limits by default 117 + 118 + ): 119 + # Load the system prompt and add it to the conversation history 120 + self.system_prompt = SYSTEM_PROMPT 121 + self.messages: list[ChatMessage] = [ 122 + ChatMessage(role="system", content=self.system_prompt) 123 + ] 124 + 125 + # Set the maximum number of tool turns and use_vllm flag 126 + self.max_tool_turns = max_tool_turns 127 + self.use_vllm = use_vllm 128 + 100 129 app = FastAPI() 130 + 131 + agent: Agent() 101 132 102 133 def get_or_load_model(model_spec: str, verbose: bool = False) -> MLXRunner: 103 134 """Get model from cache or load it if not cached.""" ··· 144 175 145 176 return _model_cache[model_path_str] 146 177 147 - async def generate_completion_stream( 148 - runner: MLXRunner, 149 - prompt: str, 150 - request: CompletionRequest 151 - ) -> AsyncGenerator[str, None]: 152 - """Generate streaming completion response.""" 153 - completion_id = f"cmpl-{uuid.uuid4()}" 154 - created = int(time.time()) 155 - 156 - # Yield initial response 157 - initial_response = { 158 - "id": completion_id, 159 - "object": "text_completion", 160 - "created": created, 161 - "model": request.model, 162 - "choices": [ 163 - { 164 - "index": 0, 165 - "text": "", 166 - "logprobs": None, 167 - "finish_reason": None 168 - } 169 - ] 170 - } 171 - 172 - yield f"data: {json.dumps(initial_response)}\n\n" 173 - 174 - # Stream tokens 175 - try: 176 - token_count = 0 177 - for token in runner.generate_streaming( 178 - prompt=prompt, 179 - max_tokens=runner.get_effective_max_tokens(request.max_tokens or _default_max_tokens, interactive=False), 180 - temperature=request.temperature, 181 - top_p=request.top_p, 182 - repetition_penalty=request.repetition_penalty, 183 - use_chat_template=False # Raw completion mode 184 - ): 185 - token_count += 1 186 - 187 - chunk_response = { 188 - "id": completion_id, 189 - "object": "text_completion", 190 - "created": created, 191 - "model": request.model, 192 - "choices": [ 193 - { 194 - "index": 0, 195 - "text": token, 196 - "logprobs": None, 197 - "finish_reason": None 198 - } 199 - ] 200 - } 201 - 202 - yield f"data: {json.dumps(chunk_response)}\n\n" 203 - 204 - # Check for stop sequences 205 - if request.stop: 206 - stop_sequences = request.stop if isinstance(request.stop, list) else [request.stop] 207 - if any(stop in token for stop in stop_sequences): 208 - break 209 - 210 - except Exception as e: 211 - error_response = { 212 - "id": completion_id, 213 - "object": "text_completion", 214 - "created": created, 215 - "model": request.model, 216 - "choices": [ 217 - { 218 - "index": 0, 219 - "text": "", 220 - "logprobs": None, 221 - "finish_reason": "error" 222 - } 223 - ], 224 - "error": str(e) 225 - } 226 - yield f"data: {json.dumps(error_response)}\n\n" 227 - 228 - # Final response 229 - final_response = { 230 - "id": completion_id, 231 - "object": "text_completion", 232 - "created": created, 233 - "model": request.model, 234 - "choices": [ 235 - { 236 - "index": 0, 237 - "text": "", 238 - "logprobs": None, 239 - "finish_reason": "stop" 240 - } 241 - ] 242 - } 243 - 244 - yield f"data: {json.dumps(final_response)}\n\n" 245 - yield "data: [DONE]\n\n" 246 - 247 - async def generate_chat_stream( 248 - runner: MLXRunner, 249 - messages: List[ChatMessage], 250 - request: ChatCompletionRequest 251 - ) -> AsyncGenerator[str, None]: 252 - """Generate streaming chat completion response.""" 253 - completion_id = f"chatcmpl-{uuid.uuid4()}" 254 - created = int(time.time()) 255 - 256 - # Convert messages to dict format for runner 257 - message_dicts = format_chat_messages_for_runner(messages) 258 - 259 - # Let the runner format with chat templates 260 - prompt = runner._format_conversation(message_dicts, use_chat_template=True) 261 - 262 - # Yield initial response 263 - initial_response = { 264 - "id": completion_id, 265 - "object": "chat.completion.chunk", 266 - "created": created, 267 - "model": request.model, 268 - "choices": [ 269 - { 270 - "index": 0, 271 - "delta": {"role": "assistant", "content": ""}, 272 - "finish_reason": None 273 - } 274 - ] 275 - } 276 - 277 - yield f"data: {json.dumps(initial_response)}\n\n" 278 - 279 - # Stream tokens 280 - try: 281 - for token in runner.generate_streaming( 282 - prompt=prompt, 283 - max_tokens=runner.get_effective_max_tokens(request.max_tokens or _default_max_tokens, interactive=False), 284 - temperature=request.temperature, 285 - top_p=request.top_p, 286 - repetition_penalty=request.repetition_penalty, 287 - use_chat_template=False, # Already applied in _format_conversation 288 - use_chat_stop_tokens=False # Server mode shouldn't stop on chat markers 289 - ): 290 - chunk_response = { 291 - "id": completion_id, 292 - "object": "chat.completion.chunk", 293 - "created": created, 294 - "model": request.model, 295 - "choices": [ 296 - { 297 - "index": 0, 298 - "delta": {"content": token}, 299 - "finish_reason": None 300 - } 301 - ] 302 - } 303 - 304 - yield f"data: {json.dumps(chunk_response)}\n\n" 305 - 306 - # Check for stop sequences 307 - if request.stop: 308 - stop_sequences = request.stop if isinstance(request.stop, list) else [request.stop] 309 - if any(stop in token for stop in stop_sequences): 310 - break 311 - 312 - except Exception as e: 313 - error_response = { 314 - "id": completion_id, 315 - "object": "chat.completion.chunk", 316 - "created": created, 317 - "model": request.model, 318 - "choices": [ 319 - { 320 - "index": 0, 321 - "delta": {}, 322 - "finish_reason": "error" 323 - } 324 - ], 325 - "error": str(e) 326 - } 327 - yield f"data: {json.dumps(error_response)}\n\n" 328 - 329 - # Final response 330 - final_response = { 331 - "id": completion_id, 332 - "object": "chat.completion.chunk", 333 - "created": created, 334 - "model": request.model, 335 - "choices": [ 336 - { 337 - "index": 0, 338 - "delta": {}, 339 - "finish_reason": "stop" 340 - } 341 - ] 342 - } 343 - 344 - yield f"data: {json.dumps(final_response)}\n\n" 345 - yield "data: [DONE]\n\n" 346 - 347 - 348 178 def format_chat_messages_for_runner(messages: List[ChatMessage]) -> List[Dict[str, str]]: 349 179 """Convert chat messages to format expected by MLXRunner. 350 180 351 181 Returns messages in dict format for the runner to apply chat templates. 352 182 """ 353 - system_message = ChatMessage(role="system", content=SYSTEM_PROMPT) 354 - messages.append(system_message) 355 183 return [{"role": msg.role, "content": msg.content} for msg in messages] 356 184 357 185 ··· 363 191 async def ping(): 364 192 return {"message": "ping"} 365 193 194 + @app.post("/start") 195 + async def start_model(request: StartRequest): 196 + """Load the model and start the agent""" 197 + global _messages, _runner 198 + print(str(request)) 199 + _messages = [ChatMessage(role="system", content=SYSTEM_PROMPT)] 200 + 201 + try: 202 + _runner = get_or_load_model(request.model) 203 + return {"message": "Model loaded"} 204 + except Exception as e: 205 + raise HTTPException(status_code=500, detail=str(e)) 366 206 367 207 @app.post("/v1/chat/completions") 368 208 async def create_chat_completion(request: ChatCompletionRequest): 369 209 """Create a chat completion.""" 210 + global _messages, _max_tool_turns 370 211 try: 371 212 runner = get_or_load_model(request.model) 372 213 ··· 383 224 created = int(time.time()) 384 225 385 226 # Convert messages to dict format for runner 386 - message_dicts = format_chat_messages_for_runner(request.messages) 387 - # conversation_history.append({"role": "system", "content": system_prompt}) 227 + # _messages.append(system_message) 228 + _messages.extend(request.messages) 229 + message_dicts = format_chat_messages_for_runner(_messages) 388 230 # Let the runner format with chat templates 389 231 prompt = runner._format_conversation(message_dicts, use_chat_template=True) 390 232 ··· 398 240 ) 399 241 400 242 # Token counting 401 - total_prompt = "\n\n".join([msg.content for msg in request.messages]) 402 - prompt_tokens = count_tokens(total_prompt) 403 - completion_tokens = count_tokens(generated_text) 243 + # total_prompt = "\n\n".join([msg.content for msg in request.messages]) 244 + # prompt_tokens = count_tokens(total_prompt) 245 + # completion_tokens = count_tokens(generated_text) 246 + 247 + thoughts = extract_thoughts(generated_text) 248 + reply = extract_reply(generated_text) 249 + python_code = extract_python_code(generated_text) 250 + print(generated_text) 251 + result = ({}, "") 252 + if python_code: 253 + create_memory_if_not_exists() 254 + result = execute_sandboxed_code( 255 + code=python_code, 256 + allowed_path=MEMORY_PATH, 257 + import_module="server.mem_agent.tools", 258 + ) 259 + 260 + print(reply) 261 + print(str(result)) 404 262 263 + remaining_tool_turns = _max_tool_turns 264 + while remaining_tool_turns > 0 and not reply: 265 + _messages.append(ChatMessage(role="user", content=format_results(result[0], result[1]))) 266 + message_dicts = format_chat_messages_for_runner(_messages) 267 + # Let the runner format with chat templates 268 + prompt = runner._format_conversation(message_dicts, use_chat_template=True) 269 + generated_text = runner.generate_batch( 270 + prompt=prompt 271 + ) 272 + print(generated_text) 273 + # Extract the thoughts, reply and python code from the response 274 + thoughts = extract_thoughts(generated_text) 275 + reply = extract_reply(generated_text) 276 + python_code = extract_python_code(generated_text) 277 + 278 + _messages.append(ChatMessage(role="assistant", content=generated_text)) 279 + if python_code: 280 + create_memory_if_not_exists() 281 + result = execute_sandboxed_code( 282 + code=python_code, 283 + allowed_path=MEMORY_PATH, 284 + import_module="server.mem_agent.tools", 285 + ) 286 + else: 287 + # Reset result when no Python code is executed 288 + result = ({}, "") 289 + remaining_tool_turns -= 1 290 + 405 291 return ChatCompletionResponse( 406 292 id=completion_id, 407 293 created=created, ··· 411 297 "index": 0, 412 298 "message": { 413 299 "role": "assistant", 414 - "content": generated_text 300 + "content": reply 415 301 }, 416 302 "finish_reason": "stop" 417 303 } 418 304 ], 419 - usage={ 420 - "prompt_tokens": prompt_tokens, 421 - "completion_tokens": completion_tokens, 422 - "total_tokens": prompt_tokens + completion_tokens 423 - } 305 + # usage={ 306 + # "prompt_tokens": prompt_tokens, 307 + # "completion_tokens": completion_tokens, 308 + # "total_tokens": prompt_tokens + completion_tokens 309 + # } 424 310 ) 425 311 except Exception as e: 426 312 raise HTTPException(status_code=500, detail=str(e))
+2 -1
server/config.py
··· 1 1 from pathlib import Path 2 - 2 + import os 3 3 PORT = 6969 4 4 MODEL_ID = "driaforall/mem-agent" 5 5 6 6 prompt_path = Path(__file__).parent / "system_prompt.txt" 7 + MEMORY_PATH = os.path.expanduser("~") + "/tiles_memory" 7 8 8 9 with open(prompt_path, "r", encoding="utf-8") as f: 9 10 SYSTEM_PROMPT = f.read().strip()
+1
server/mem_agent/__init__.py
··· 1 +
+334
server/mem_agent/engine.py
··· 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 + SANDBOX_TIMEOUT = 10 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", "mem_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 + print("stderr:", result.stderr.decode()) 304 + print("stdout:", result.stdout[:200]) 305 + 306 + try: 307 + local_vars, error_msg = pickle.loads(result.stdout) 308 + except Exception as e: 309 + return None, f"Failed to decode sandbox output: {e}" 310 + 311 + if error_msg is None: 312 + error_msg = "" 313 + 314 + return local_vars, error_msg 315 + 316 + def _subprocess_entry() -> None: 317 + """Entry point for sandbox subprocess.""" 318 + params_b64 = os.environ.get("SANDBOX_PARAMS") 319 + if not params_b64: 320 + sys.exit(1) 321 + params = pickle.loads(base64.b64decode(params_b64)) 322 + locals_dict, error = _run_user_code( 323 + params["code"], 324 + params.get("allow_installs", False), 325 + params.get("allowed_path"), 326 + params.get("blacklist", []), 327 + params.get("available_functions", {}), 328 + params.get("log", False), 329 + ) 330 + sys.stdout.buffer.write(pickle.dumps((locals_dict, error))) 331 + 332 + 333 + if __name__ == "__main__": 334 + _subprocess_entry()
+359
server/mem_agent/tools.py
··· 1 + import os 2 + import tempfile 3 + import uuid 4 + import subprocess 5 + from pathlib import Path 6 + from typing import Union 7 + 8 + from server.mem_agent.utils import MEMORY_PATH, check_size_limits, create_memory_if_not_exists 9 + 10 + def get_size(file_or_dir_path: str) -> int: 11 + """ 12 + Get the size of a file or directory. 13 + 14 + Args: 15 + file_or_dir_path: The path to the file or directory. 16 + If empty string, returns total memory directory size. 17 + 18 + Returns: 19 + The size of the file or directory in bytes. 20 + """ 21 + # Handle empty string by returning total memory size 22 + if not file_or_dir_path or file_or_dir_path == "": 23 + # Get the current working directory (which should be the memory root) 24 + cwd = os.getcwd() 25 + total_size = 0 26 + for dirpath, dirnames, filenames in os.walk(cwd): 27 + for filename in filenames: 28 + file_path = os.path.join(dirpath, filename) 29 + try: 30 + total_size += os.path.getsize(file_path) 31 + except OSError: 32 + pass 33 + return total_size 34 + 35 + # Otherwise check the specific path 36 + if os.path.isfile(file_or_dir_path): 37 + return os.path.getsize(file_or_dir_path) 38 + elif os.path.isdir(file_or_dir_path): 39 + total_size = 0 40 + for dirpath, dirnames, filenames in os.walk(file_or_dir_path): 41 + for filename in filenames: 42 + file_path = os.path.join(dirpath, filename) 43 + try: 44 + total_size += os.path.getsize(file_path) 45 + except OSError: 46 + pass 47 + return total_size 48 + else: 49 + raise FileNotFoundError(f"Path not found: {file_or_dir_path}") 50 + 51 + def create_file(file_path: str, content: str = "") -> bool: 52 + """ 53 + Create a new file in the memory with the given content (if any). 54 + First create a temporary file with the given content, check if 55 + the size limits are respected, if so, move the temporary file to 56 + the final destination. 57 + 58 + Args: 59 + file_path: The path to the file. 60 + content: The content of the file. 61 + 62 + Returns: 63 + True if the file was created successfully, False otherwise. 64 + """ 65 + temp_file_path = None 66 + try: 67 + # Create parent directories if they don't exist 68 + parent_dir = os.path.dirname(file_path) 69 + if parent_dir and not os.path.exists(parent_dir): 70 + os.makedirs(parent_dir, exist_ok=True) 71 + 72 + # Create a unique temporary file name in the same directory as the target file 73 + # This ensures the temp file is within the sandbox's allowed path 74 + target_dir = os.path.dirname(os.path.abspath(file_path)) or "." 75 + temp_file_path = os.path.join(target_dir, f"temp_{uuid.uuid4().hex[:8]}.txt") 76 + 77 + with open(temp_file_path, "w") as f: 78 + f.write(content) 79 + 80 + if check_size_limits(temp_file_path): 81 + # Move the content to the final destination 82 + with open(file_path, "w") as f: 83 + f.write(content) 84 + os.remove(temp_file_path) 85 + return True 86 + else: 87 + os.remove(temp_file_path) 88 + raise Exception(f"File {file_path} is too large to create") 89 + except Exception as e: 90 + # Clean up temp file if it exists 91 + if temp_file_path and os.path.exists(temp_file_path): 92 + try: 93 + os.remove(temp_file_path) 94 + except Exception as e: 95 + raise Exception(f"Error removing temp file {temp_file_path}: {e}") 96 + raise Exception(f"Error creating file {file_path}: {e}") 97 + 98 + def create_dir(dir_path: str) -> bool: 99 + """ 100 + Create a new directory in the memory. 101 + 102 + Args: 103 + dir_path: The path to the directory. 104 + 105 + Returns: 106 + True if the directory was created successfully, False otherwise. 107 + """ 108 + try: 109 + os.makedirs(dir_path, exist_ok=True) 110 + return True 111 + except Exception: 112 + return False 113 + 114 + 115 + def update_file(file_path: str, old_content: str, new_content: str) -> Union[bool, str]: 116 + """ 117 + Simple find-and-replace update method for files. 118 + 119 + This is an easier alternative to write_to_file() that doesn't require 120 + creating git-style diffs. It performs a simple string replacement. 121 + 122 + Parameters 123 + ---------- 124 + file_path : str 125 + Path to the file to update. 126 + old_content : str 127 + The exact text to find and replace in the file. 128 + new_content : str 129 + The text to replace old_content with. 130 + 131 + Returns 132 + ------- 133 + Union[bool, str] 134 + True if successful, error message string if failed. 135 + 136 + Examples 137 + -------- 138 + # Add a new row to a table 139 + old = "| TKT-1056 | 2024-09-25 | Late Delivery | Resolved |" 140 + new = "| TKT-1056 | 2024-09-25 | Late Delivery | Resolved |\\n| TKT-1057 | 2024-11-11 | Damaged Item | Open |" 141 + result = update_file("user.md", old, new) 142 + """ 143 + try: 144 + # Read the current file content 145 + if not os.path.exists(file_path): 146 + return f"Error: File '{file_path}' does not exist" 147 + 148 + if not os.path.isfile(file_path): 149 + return f"Error: '{file_path}' is not a file" 150 + 151 + with open(file_path, "r") as f: 152 + current_content = f.read() 153 + 154 + # Check if old_content exists in the file 155 + if old_content not in current_content: 156 + # Provide helpful context about what wasn't found 157 + preview_length = 50 158 + preview = old_content[:preview_length] + "..." if len(old_content) > preview_length else old_content 159 + return f"Error: Could not find the specified content in the file. Looking for: '{preview}'" 160 + 161 + # Count occurrences to warn about multiple matches 162 + occurrences = current_content.count(old_content) 163 + if occurrences > 1: 164 + # Still proceed but warn the user 165 + print(f"Warning: Found {occurrences} occurrences of the content. Replacing only the first one.") 166 + 167 + # Perform the replacement (only first occurrence) 168 + updated_content = current_content.replace(old_content, new_content, 1) 169 + 170 + # Check if replacement actually changed anything 171 + if updated_content == current_content: 172 + return "Error: No changes were made to the file" 173 + 174 + # Write the updated content back 175 + with open(file_path, "w") as f: 176 + f.write(updated_content) 177 + 178 + return True 179 + 180 + except PermissionError: 181 + return f"Error: Permission denied writing to '{file_path}'" 182 + except Exception as e: 183 + return f"Error: Unexpected error - {str(e)}" 184 + 185 + def read_file(file_path: str) -> str: 186 + """ 187 + Read a file in the memory. 188 + 189 + Args: 190 + file_path: The path to the file. 191 + 192 + Returns: 193 + The content of the file, or an error message if the file cannot be read. 194 + """ 195 + try: 196 + # Ensure the file path is properly resolved 197 + if not os.path.exists(file_path): 198 + return f"Error: File {file_path} does not exist" 199 + 200 + if not os.path.isfile(file_path): 201 + return f"Error: {file_path} is not a file" 202 + 203 + with open(file_path, "r") as f: 204 + return f.read() 205 + except PermissionError: 206 + return f"Error: Permission denied accessing {file_path}" 207 + except Exception as e: 208 + return f"Error: {e}" 209 + 210 + def list_files() -> str: 211 + """ 212 + Display all files and directories in the current working directory as a tree structure. 213 + 214 + Example output: 215 + ``` 216 + ./ 217 + ├── user.md 218 + └── entities/ 219 + ├── 452_willow_creek_dr.md 220 + └── frank_miller_plumbing.md 221 + ``` 222 + 223 + Returns: 224 + A string representation of the directory tree. 225 + """ 226 + try: 227 + # Always use current working directory 228 + dir_path = os.getcwd() 229 + 230 + def build_tree(start_path, prefix="", is_last=True): 231 + """Recursively build tree structure""" 232 + entries = [] 233 + try: 234 + items = sorted(os.listdir(start_path)) 235 + # Filter out hidden files and __pycache__ 236 + items = [item for item in items if not item.startswith('.') and item != '__pycache__'] 237 + except PermissionError: 238 + return f"{prefix}[Permission Denied]\n" 239 + 240 + if not items: 241 + return "" 242 + 243 + for i, item in enumerate(items): 244 + item_path = os.path.join(start_path, item) 245 + is_last_item = i == len(items) - 1 246 + 247 + # Choose the right prefix characters 248 + if is_last_item: 249 + current_prefix = prefix + "└── " 250 + extension = prefix + " " 251 + else: 252 + current_prefix = prefix + "├── " 253 + extension = prefix + "│ " 254 + 255 + if os.path.isdir(item_path): 256 + # Check if directory is empty 257 + try: 258 + dir_contents = [f for f in os.listdir(item_path) 259 + if not f.startswith('.') and f != '__pycache__'] 260 + if not dir_contents: 261 + entries.append(f"{current_prefix}{item}/ (empty)\n") 262 + else: 263 + entries.append(f"{current_prefix}{item}/\n") 264 + # Recursively add subdirectory contents 265 + entries.append(build_tree(item_path, extension, is_last_item)) 266 + except PermissionError: 267 + entries.append(f"{current_prefix}{item}/ [Permission Denied]\n") 268 + else: 269 + entries.append(f"{current_prefix}{item}\n") 270 + 271 + return "".join(entries) 272 + 273 + # Start with the root directory 274 + tree = f"./\n{build_tree(dir_path)}" 275 + return tree.rstrip() # Remove trailing newline 276 + 277 + except Exception as e: 278 + return f"Error: {e}" 279 + 280 + def delete_file(file_path: str) -> bool: 281 + """ 282 + Delete a file in the memory. 283 + 284 + Args: 285 + file_path: The path to the file. 286 + 287 + Returns: 288 + True if the file was deleted successfully, False otherwise. 289 + """ 290 + try: 291 + os.remove(file_path) 292 + return True 293 + except Exception: 294 + return False 295 + 296 + def go_to_link(link_string: str) -> str: 297 + """ 298 + Go to a link in the memory and return the content of the note Y. A link in a note X to a note Y, with the 299 + path path/to/note/Y.md, is structured like this: 300 + [[path/to/note/Y]] 301 + 302 + Args: 303 + link_string: The link to go to. 304 + 305 + Returns: 306 + The content of the note Y, or an error message if the link cannot be accessed. 307 + """ 308 + try: 309 + # Handle Obsidian-style links: [[path/to/note]] -> path/to/note.md 310 + if link_string.startswith("[[") and link_string.endswith("]]"): 311 + file_path = link_string[2:-2] # Remove [[ and ]] 312 + if not file_path.endswith('.md'): 313 + file_path += '.md' 314 + else: 315 + file_path = link_string 316 + 317 + # Ensure the file path is properly resolved 318 + if not os.path.exists(file_path): 319 + return f"Error: File {file_path} not found" 320 + 321 + if not os.path.isfile(file_path): 322 + return f"Error: {file_path} is not a file" 323 + 324 + with open(file_path, "r") as f: 325 + return f.read() 326 + except PermissionError: 327 + return f"Error: Permission denied accessing {link_string}" 328 + except Exception as e: 329 + return f"Error: {e}" 330 + 331 + def check_if_file_exists(file_path: str) -> bool: 332 + """ 333 + Check if a file exists in the given filepath. 334 + 335 + Args: 336 + file_path: The path to the file. 337 + 338 + Returns: 339 + True if the file exists and is a file, False otherwise. 340 + """ 341 + try: 342 + return os.path.exists(file_path) and os.path.isfile(file_path) 343 + except (OSError, TypeError, ValueError): 344 + return False 345 + 346 + def check_if_dir_exists(dir_path: str) -> bool: 347 + """ 348 + Check if a directory exists in the given filepath. 349 + 350 + Args: 351 + dir_path: The path to the directory. 352 + 353 + Returns: 354 + True if the directory exists and is a directory, False otherwise. 355 + """ 356 + try: 357 + return os.path.exists(dir_path) and os.path.isdir(dir_path) 358 + except (OSError, TypeError, ValueError): 359 + return False
+203
server/mem_agent/utils.py
··· 1 + import os 2 + import shutil 3 + 4 + import black 5 + from ..config import MEMORY_PATH 6 + 7 + # Memory 8 + FILE_SIZE_LIMIT = 1024 * 1024 # 1MB 9 + DIR_SIZE_LIMIT = 1024 * 1024 * 10 # 10MB 10 + MEMORY_SIZE_LIMIT = 1024 * 1024 * 100 # 100MB 11 + 12 + 13 + def check_file_size_limit(file_path: str) -> bool: 14 + """ 15 + Check if the file size limit is respected. 16 + """ 17 + return os.path.getsize(file_path) <= FILE_SIZE_LIMIT 18 + 19 + 20 + def check_dir_size_limit(dir_path: str) -> bool: 21 + """ 22 + Check if the directory size limit is respected. 23 + """ 24 + return os.path.getsize(dir_path) <= DIR_SIZE_LIMIT 25 + 26 + 27 + def check_memory_size_limit() -> bool: 28 + """ 29 + Check if the memory size limit is respected. 30 + """ 31 + current_working_dir = os.getcwd() 32 + return os.path.getsize(current_working_dir) <= MEMORY_SIZE_LIMIT 33 + 34 + 35 + def check_size_limits(file_or_dir_path: str) -> bool: 36 + """ 37 + Check if the size limits are respected. 38 + """ 39 + if file_or_dir_path == "": 40 + return check_memory_size_limit() 41 + elif os.path.isdir(file_or_dir_path): 42 + return check_dir_size_limit(file_or_dir_path) and check_memory_size_limit() 43 + elif os.path.isfile(file_or_dir_path): 44 + parent_dir = os.path.dirname(file_or_dir_path) 45 + if not parent_dir == "": 46 + return ( 47 + check_file_size_limit(file_or_dir_path) 48 + and check_dir_size_limit(parent_dir) 49 + and check_memory_size_limit() 50 + ) 51 + else: 52 + return check_file_size_limit(file_or_dir_path) and check_memory_size_limit() 53 + else: 54 + return False 55 + 56 + 57 + def create_memory_if_not_exists(path: str = MEMORY_PATH): 58 + """ 59 + Create the memory if it doesn't exist. 60 + 61 + Args: 62 + path: The path to create. Defaults to MEMORY_PATH. 63 + 64 + Returns: 65 + None 66 + """ 67 + try: 68 + if not os.path.exists(path): 69 + os.makedirs(path, exist_ok=True) 70 + except Exception as e: 71 + print(f"Error creating memory directory at {path}: {e}") 72 + 73 + 74 + def delete_memory(path: str = MEMORY_PATH) -> None: 75 + """ 76 + Delete the memory. 77 + 78 + Args: 79 + path: The path to delete. Defaults to MEMORY_PATH. 80 + """ 81 + if os.path.exists(path): 82 + shutil.rmtree(path) 83 + 84 + 85 + def _format_python_code_with_black(code: str) -> str: 86 + """ 87 + Format Python code using Black formatter. 88 + 89 + Args: 90 + code: The Python code to format 91 + 92 + Returns: 93 + The formatted Python code, or original code if formatting fails 94 + """ 95 + if not code.strip(): 96 + return code 97 + 98 + try: 99 + # For incomplete code fragments, wrap them in a function to make them valid Python 100 + # This helps Black parse and format them correctly 101 + lines = code.strip().split('\n') 102 + 103 + # Check if code looks like complete statements or just expressions/fragments 104 + needs_wrapping = True 105 + for line in lines: 106 + stripped = line.strip() 107 + if (stripped.startswith(('def ', 'class ', 'import ', 'from ')) or 108 + stripped.startswith(('if ', 'for ', 'while ', 'try:', 'with ')) or 109 + '=' in stripped or stripped.startswith(('print(', 'return '))): 110 + needs_wrapping = False 111 + break 112 + 113 + if needs_wrapping: 114 + # Wrap in a function to make it valid Python for Black 115 + wrapped_code = f"def temp_function():\n" + "\n".join(f" {line}" for line in lines) 116 + 117 + try: 118 + formatted_wrapped = black.format_str( 119 + wrapped_code, 120 + mode=black.FileMode( 121 + line_length=88, 122 + string_normalization=True, 123 + is_pyi=False, 124 + ) 125 + ) 126 + # Extract the formatted content back out, removing the wrapper 127 + formatted_lines = formatted_wrapped.split('\n')[1:] # Skip "def temp_function():" 128 + formatted_code = '\n'.join(line[4:] if line.startswith(' ') else line 129 + for line in formatted_lines if line.strip()).strip() 130 + return formatted_code 131 + except: 132 + # If wrapping fails, try formatting as-is 133 + pass 134 + 135 + # Try formatting the code as-is 136 + formatted_code = black.format_str( 137 + code, 138 + mode=black.FileMode( 139 + line_length=88, 140 + string_normalization=True, 141 + is_pyi=False, 142 + ) 143 + ) 144 + return formatted_code 145 + 146 + except (black.InvalidInput, ValueError, SyntaxError, Exception) as e: 147 + # If Black fails to format (e.g., invalid syntax), return original code 148 + # This ensures we don't break the training pipeline 149 + return code 150 + 151 + 152 + def extract_python_code(response: str) -> str: 153 + """ 154 + Extract the python code from the response and format it with Black. 155 + 156 + Args: 157 + response: The response from the model. 158 + 159 + Returns: 160 + The formatted python code from the response. 161 + """ 162 + if "<python>" in response and "</python>" in response: 163 + response = response.split("<python>")[1].split("</python>")[0] 164 + if "```" in response: 165 + code = response.split("```")[1].split("```")[0] 166 + else: 167 + code = response 168 + 169 + # Format the extracted code with Black 170 + return _format_python_code_with_black(code) 171 + else: 172 + return "" 173 + 174 + 175 + def extract_reply(response: str) -> str: 176 + """ 177 + Extract the reply from the response. 178 + """ 179 + if "<reply>" in response and "</reply>" in response: 180 + return response.split("<reply>")[1].split("</reply>")[0] 181 + else: 182 + return "" 183 + 184 + 185 + def extract_thoughts(response: str) -> str: 186 + """ 187 + Extract the thoughts from the response. 188 + """ 189 + if "<think>" in response and "</think>" in response: 190 + return response.split("<think>")[1].split("</think>")[0] 191 + else: 192 + return "" 193 + 194 + 195 + def format_results(results: dict, error_msg: str = "") -> str: 196 + """ 197 + Format the results into a string. 198 + """ 199 + return ( 200 + "<result>\n(" + str(results) + ", {" + error_msg + "})\n</result>" 201 + if error_msg 202 + else "<result>\n" + str(results) + "\n</result>" 203 + )
+2 -1
server/pyproject.toml
··· 6 6 dependencies = [ 7 7 "fastapi", 8 8 "uvicorn", 9 - "mlx-lm" 9 + "mlx-lm", 10 + "black" 10 11 ] 11 12 12 13 [build-system]
+122
server/uv.lock
··· 31 31 ] 32 32 33 33 [[package]] 34 + name = "black" 35 + version = "25.9.0" 36 + source = { registry = "https://pypi.org/simple" } 37 + dependencies = [ 38 + { name = "click" }, 39 + { name = "mypy-extensions" }, 40 + { name = "packaging" }, 41 + { name = "pathspec" }, 42 + { name = "platformdirs" }, 43 + { name = "pytokens" }, 44 + { name = "tomli", marker = "python_full_version < '3.11'" }, 45 + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 46 + ] 47 + sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } 48 + wheels = [ 49 + { url = "https://files.pythonhosted.org/packages/25/40/dbe31fc56b218a858c8fc6f5d8d3ba61c1fa7e989d43d4a4574b8b992840/black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7", size = 1715605, upload-time = "2025-09-19T00:36:13.483Z" }, 50 + { url = "https://files.pythonhosted.org/packages/92/b2/f46800621200eab6479b1f4c0e3ede5b4c06b768e79ee228bc80270bcc74/black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92", size = 1571829, upload-time = "2025-09-19T00:32:42.13Z" }, 51 + { url = "https://files.pythonhosted.org/packages/4e/64/5c7f66bd65af5c19b4ea86062bb585adc28d51d37babf70969e804dbd5c2/black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713", size = 1631888, upload-time = "2025-09-19T00:30:54.212Z" }, 52 + { url = "https://files.pythonhosted.org/packages/3b/64/0b9e5bfcf67db25a6eef6d9be6726499a8a72ebab3888c2de135190853d3/black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1", size = 1327056, upload-time = "2025-09-19T00:31:08.877Z" }, 53 + { url = "https://files.pythonhosted.org/packages/b7/f4/7531d4a336d2d4ac6cc101662184c8e7d068b548d35d874415ed9f4116ef/black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa", size = 1698727, upload-time = "2025-09-19T00:31:14.264Z" }, 54 + { url = "https://files.pythonhosted.org/packages/28/f9/66f26bfbbf84b949cc77a41a43e138d83b109502cd9c52dfc94070ca51f2/black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d", size = 1555679, upload-time = "2025-09-19T00:31:29.265Z" }, 55 + { url = "https://files.pythonhosted.org/packages/bf/59/61475115906052f415f518a648a9ac679d7afbc8da1c16f8fdf68a8cebed/black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608", size = 1617453, upload-time = "2025-09-19T00:30:42.24Z" }, 56 + { url = "https://files.pythonhosted.org/packages/7f/5b/20fd5c884d14550c911e4fb1b0dae00d4abb60a4f3876b449c4d3a9141d5/black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f", size = 1333655, upload-time = "2025-09-19T00:30:56.715Z" }, 57 + { url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" }, 58 + { url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" }, 59 + { url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" }, 60 + { url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" }, 61 + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, 62 + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, 63 + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, 64 + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, 65 + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, 66 + ] 67 + 68 + [[package]] 34 69 name = "certifi" 35 70 version = "2025.10.5" 36 71 source = { registry = "https://pypi.org/simple" } ··· 393 428 ] 394 429 395 430 [[package]] 431 + name = "mypy-extensions" 432 + version = "1.1.0" 433 + source = { registry = "https://pypi.org/simple" } 434 + sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 435 + wheels = [ 436 + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 437 + ] 438 + 439 + [[package]] 396 440 name = "numpy" 397 441 version = "2.2.6" 398 442 source = { registry = "https://pypi.org/simple" } ··· 551 595 ] 552 596 553 597 [[package]] 598 + name = "pathspec" 599 + version = "0.12.1" 600 + source = { registry = "https://pypi.org/simple" } 601 + sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 602 + wheels = [ 603 + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 604 + ] 605 + 606 + [[package]] 607 + name = "platformdirs" 608 + version = "4.5.0" 609 + source = { registry = "https://pypi.org/simple" } 610 + sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } 611 + wheels = [ 612 + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, 613 + ] 614 + 615 + [[package]] 554 616 name = "protobuf" 555 617 version = "6.33.0" 556 618 source = { registry = "https://pypi.org/simple" } ··· 692 754 { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, 693 755 { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, 694 756 { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, 757 + ] 758 + 759 + [[package]] 760 + name = "pytokens" 761 + version = "0.2.0" 762 + source = { registry = "https://pypi.org/simple" } 763 + sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } 764 + wheels = [ 765 + { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, 695 766 ] 696 767 697 768 [[package]] ··· 907 978 version = "0.1.0" 908 979 source = { editable = "." } 909 980 dependencies = [ 981 + { name = "black" }, 910 982 { name = "fastapi" }, 911 983 { name = "mlx-lm" }, 912 984 { name = "uvicorn" }, ··· 914 986 915 987 [package.metadata] 916 988 requires-dist = [ 989 + { name = "black" }, 917 990 { name = "fastapi" }, 918 991 { name = "mlx-lm" }, 919 992 { name = "uvicorn" }, ··· 964 1037 { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, 965 1038 { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, 966 1039 { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, 1040 + ] 1041 + 1042 + [[package]] 1043 + name = "tomli" 1044 + version = "2.3.0" 1045 + source = { registry = "https://pypi.org/simple" } 1046 + sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } 1047 + wheels = [ 1048 + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, 1049 + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, 1050 + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, 1051 + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, 1052 + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, 1053 + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, 1054 + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, 1055 + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, 1056 + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, 1057 + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, 1058 + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, 1059 + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, 1060 + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, 1061 + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, 1062 + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, 1063 + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, 1064 + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, 1065 + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, 1066 + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, 1067 + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, 1068 + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, 1069 + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, 1070 + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, 1071 + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, 1072 + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, 1073 + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, 1074 + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, 1075 + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, 1076 + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, 1077 + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, 1078 + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, 1079 + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, 1080 + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, 1081 + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, 1082 + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, 1083 + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, 1084 + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, 1085 + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, 1086 + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, 1087 + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, 1088 + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, 967 1089 ] 968 1090 969 1091 [[package]]