jl-ecms-client 0.2.8__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.

Potentially problematic release.


This version of jl-ecms-client might be problematic. Click here for more details.

Files changed (53) hide show
  1. jl_ecms_client-0.2.8.dist-info/METADATA +295 -0
  2. jl_ecms_client-0.2.8.dist-info/RECORD +53 -0
  3. jl_ecms_client-0.2.8.dist-info/WHEEL +5 -0
  4. jl_ecms_client-0.2.8.dist-info/licenses/LICENSE +190 -0
  5. jl_ecms_client-0.2.8.dist-info/top_level.txt +1 -0
  6. mirix/client/__init__.py +14 -0
  7. mirix/client/client.py +405 -0
  8. mirix/client/constants.py +60 -0
  9. mirix/client/remote_client.py +1136 -0
  10. mirix/client/utils.py +34 -0
  11. mirix/helpers/__init__.py +1 -0
  12. mirix/helpers/converters.py +429 -0
  13. mirix/helpers/datetime_helpers.py +90 -0
  14. mirix/helpers/json_helpers.py +47 -0
  15. mirix/helpers/message_helpers.py +74 -0
  16. mirix/helpers/tool_rule_solver.py +166 -0
  17. mirix/schemas/__init__.py +1 -0
  18. mirix/schemas/agent.py +401 -0
  19. mirix/schemas/block.py +188 -0
  20. mirix/schemas/cloud_file_mapping.py +29 -0
  21. mirix/schemas/embedding_config.py +114 -0
  22. mirix/schemas/enums.py +69 -0
  23. mirix/schemas/environment_variables.py +82 -0
  24. mirix/schemas/episodic_memory.py +170 -0
  25. mirix/schemas/file.py +57 -0
  26. mirix/schemas/health.py +10 -0
  27. mirix/schemas/knowledge_vault.py +181 -0
  28. mirix/schemas/llm_config.py +187 -0
  29. mirix/schemas/memory.py +318 -0
  30. mirix/schemas/message.py +1315 -0
  31. mirix/schemas/mirix_base.py +107 -0
  32. mirix/schemas/mirix_message.py +411 -0
  33. mirix/schemas/mirix_message_content.py +230 -0
  34. mirix/schemas/mirix_request.py +39 -0
  35. mirix/schemas/mirix_response.py +183 -0
  36. mirix/schemas/openai/__init__.py +1 -0
  37. mirix/schemas/openai/chat_completion_request.py +122 -0
  38. mirix/schemas/openai/chat_completion_response.py +144 -0
  39. mirix/schemas/openai/chat_completions.py +127 -0
  40. mirix/schemas/openai/embedding_response.py +11 -0
  41. mirix/schemas/openai/openai.py +229 -0
  42. mirix/schemas/organization.py +38 -0
  43. mirix/schemas/procedural_memory.py +151 -0
  44. mirix/schemas/providers.py +816 -0
  45. mirix/schemas/resource_memory.py +134 -0
  46. mirix/schemas/sandbox_config.py +132 -0
  47. mirix/schemas/semantic_memory.py +162 -0
  48. mirix/schemas/source.py +96 -0
  49. mirix/schemas/step.py +53 -0
  50. mirix/schemas/tool.py +241 -0
  51. mirix/schemas/tool_rule.py +209 -0
  52. mirix/schemas/usage.py +31 -0
  53. mirix/schemas/user.py +67 -0
@@ -0,0 +1,1136 @@
1
+ """
2
+ MirixClient implementation for Mirix.
3
+ This client communicates with a remote Mirix server via REST API.
4
+ """
5
+
6
+ import os
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
+
9
+ import requests
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
12
+
13
+ from mirix.client.client import AbstractClient
14
+ from mirix.constants import FUNCTION_RETURN_CHAR_LIMIT
15
+ from mirix.schemas.agent import AgentState, AgentType, CreateAgent, CreateMetaAgent
16
+ from mirix.schemas.block import Block, BlockUpdate, CreateBlock, Human, Persona
17
+ from mirix.schemas.embedding_config import EmbeddingConfig
18
+ from mirix.schemas.environment_variables import (
19
+ SandboxEnvironmentVariable,
20
+ SandboxEnvironmentVariableCreate,
21
+ SandboxEnvironmentVariableUpdate,
22
+ )
23
+ from mirix.schemas.file import FileMetadata
24
+ from mirix.schemas.llm_config import LLMConfig
25
+ from mirix.schemas.memory import ArchivalMemorySummary, Memory, RecallMemorySummary
26
+ from mirix.schemas.message import Message, MessageCreate
27
+ from mirix.schemas.mirix_response import MirixResponse
28
+ from mirix.schemas.organization import Organization
29
+ from mirix.schemas.sandbox_config import (
30
+ E2BSandboxConfig,
31
+ LocalSandboxConfig,
32
+ SandboxConfig,
33
+ SandboxConfigCreate,
34
+ SandboxConfigUpdate,
35
+ )
36
+ from mirix.schemas.tool import Tool, ToolCreate, ToolUpdate
37
+ from mirix.schemas.tool_rule import BaseToolRule
38
+ from mirix.log import get_logger
39
+
40
+ logger = get_logger(__name__)
41
+
42
+
43
+ class MirixClient(AbstractClient):
44
+ """
45
+ Client that communicates with a remote Mirix server via REST API.
46
+
47
+ This client runs on the user's local machine and makes HTTP requests
48
+ to a Mirix server hosted in the cloud.
49
+
50
+ Example:
51
+ >>> client = MirixClient(
52
+ ... base_url="https://api.mirix.ai",
53
+ ... user_id="my-user",
54
+ ... org_id="my-org",
55
+ ... )
56
+ >>> agent = client.create_agent(name="my_agent")
57
+ >>> response = client.send_message(
58
+ ... agent_id=agent.id,
59
+ ... message="Hello!",
60
+ ... role="user"
61
+ ... )
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ api_key: Optional[str] = None,
67
+ base_url: Optional[str] = None,
68
+ user_id: Optional[str] = None,
69
+ user_name: Optional[str] = None,
70
+ org_id: Optional[str] = None,
71
+ org_name: Optional[str] = None,
72
+ debug: bool = False,
73
+ timeout: int = 60,
74
+ max_retries: int = 3,
75
+ ):
76
+ """
77
+ Initialize MirixClient.
78
+
79
+ Args:
80
+ base_url: Base URL of the Mirix API server (optional, can also be set via MIRIX_API_URL env var, default: "http://localhost:8000")
81
+ user_id: User ID (optional, will be auto-generated if not provided)
82
+ user_name: User name (optional, defaults to user_id if not provided)
83
+ org_id: Organization ID (optional, will be auto-generated if not provided)
84
+ org_name: Organization name (optional, defaults to org_id if not provided)
85
+ debug: Whether to enable debug logging
86
+ timeout: Request timeout in seconds
87
+ max_retries: Number of retries for failed requests
88
+ """
89
+ super().__init__(debug=debug)
90
+
91
+ # Get base URL from parameter or environment variable
92
+ self.base_url = (base_url or os.environ.get("MIRIX_API_URL", "http://localhost:8000")).rstrip("/")
93
+
94
+ # Generate IDs if not provided
95
+ if not user_id:
96
+ import uuid
97
+ user_id = f"user-{uuid.uuid4().hex[:8]}"
98
+
99
+ if not org_id:
100
+ import uuid
101
+ org_id = f"org-{uuid.uuid4().hex[:8]}"
102
+
103
+ self.user_id = user_id
104
+ self.user_name = user_name or user_id
105
+ self.org_id = org_id
106
+ self.org_name = org_name or org_id
107
+ self.timeout = timeout
108
+
109
+ # Track initialized meta agent for this project
110
+ self._meta_agent: Optional[AgentState] = None
111
+
112
+ # Create session with retry logic
113
+ self.session = requests.Session()
114
+
115
+ # Configure retries
116
+ retry_strategy = Retry(
117
+ total=max_retries,
118
+ backoff_factor=1,
119
+ status_forcelist=[429, 500, 502, 503, 504],
120
+ allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"],
121
+ )
122
+ adapter = HTTPAdapter(max_retries=retry_strategy)
123
+ self.session.mount("http://", adapter)
124
+ self.session.mount("https://", adapter)
125
+
126
+ # Set headers
127
+ if self.user_id:
128
+ self.session.headers.update({"X-User-ID": self.user_id})
129
+
130
+ if self.org_id:
131
+ self.session.headers.update({"X-Org-ID": self.org_id})
132
+
133
+ self.session.headers.update({"Content-Type": "application/json"})
134
+
135
+ # Create organization and user if they don't exist
136
+ self._ensure_org_and_user_exist()
137
+
138
+ def _ensure_org_and_user_exist(self):
139
+ """
140
+ Ensure that the organization and user exist on the server.
141
+ Creates them if they don't exist.
142
+ """
143
+ try:
144
+
145
+ # Create or get organization first
146
+ org_response = self._request(
147
+ "POST",
148
+ "/organizations/create_or_get",
149
+ json={"org_id": self.org_id, "name": self.org_name}
150
+ )
151
+ if self.debug:
152
+ logger.debug("[MirixClient] Organization initialized: %s (name: %s)", self.org_id, self.org_name)
153
+
154
+ # Create or get user
155
+ user_response = self._request(
156
+ "POST",
157
+ "/users/create_or_get",
158
+ json={
159
+ "user_id": self.user_id,
160
+ "name": self.user_name,
161
+ "org_id": self.org_id
162
+ }
163
+ )
164
+ if self.debug:
165
+ logger.debug("[MirixClient] User initialized: %s (name: %s)", self.user_id, self.user_name)
166
+ except Exception as e:
167
+ # Don't fail initialization if this fails - the server might handle it
168
+ if self.debug:
169
+ logger.debug("[MirixClient] Note: Could not pre-create user/org: %s", e)
170
+ logger.debug("[MirixClient] Server will create them on first request if needed")
171
+
172
+ def _request(
173
+ self,
174
+ method: str,
175
+ endpoint: str,
176
+ json: Optional[Dict] = None,
177
+ params: Optional[Dict] = None,
178
+ ) -> Any:
179
+ """
180
+ Make an HTTP request to the API.
181
+
182
+ Args:
183
+ method: HTTP method (GET, POST, etc.)
184
+ endpoint: API endpoint (e.g., "/agents")
185
+ json: JSON body for the request
186
+ params: Query parameters
187
+
188
+ Returns:
189
+ Response data (parsed JSON)
190
+
191
+ Raises:
192
+ requests.HTTPError: If the request fails
193
+ """
194
+ url = f"{self.base_url}{endpoint}"
195
+
196
+ if self.debug:
197
+ logger.debug("[MirixClient] %s %s", method, url)
198
+ if json:
199
+ logger.debug("[MirixClient] Request body: %s", json)
200
+
201
+ response = self.session.request(
202
+ method=method,
203
+ url=url,
204
+ json=json,
205
+ params=params,
206
+ timeout=self.timeout,
207
+ )
208
+
209
+ try:
210
+ response.raise_for_status()
211
+ except requests.HTTPError as e:
212
+ # Try to extract error message from response
213
+ try:
214
+ error_detail = response.json().get("detail", str(e))
215
+ except:
216
+ error_detail = str(e)
217
+ raise requests.HTTPError(f"API request failed: {error_detail}") from e
218
+
219
+ # Return parsed JSON if there's content
220
+ if response.content:
221
+ return response.json()
222
+ return None
223
+
224
+ # ========================================================================
225
+ # Agent Methods
226
+ # ========================================================================
227
+
228
+ def list_agents(
229
+ self,
230
+ query_text: Optional[str] = None,
231
+ tags: Optional[List[str]] = None,
232
+ limit: int = 100,
233
+ cursor: Optional[str] = None,
234
+ parent_id: Optional[str] = None,
235
+ ) -> List[AgentState]:
236
+ """List all agents."""
237
+ params = {"limit": limit}
238
+ if query_text:
239
+ params["query_text"] = query_text
240
+ if tags:
241
+ params["tags"] = ",".join(tags)
242
+ if cursor:
243
+ params["cursor"] = cursor
244
+ if parent_id:
245
+ params["parent_id"] = parent_id
246
+
247
+ data = self._request("GET", "/agents", params=params)
248
+ return [AgentState(**agent) for agent in data]
249
+
250
+ def agent_exists(
251
+ self, agent_id: Optional[str] = None, agent_name: Optional[str] = None
252
+ ) -> bool:
253
+ """Check if an agent exists."""
254
+ if not (agent_id or agent_name):
255
+ raise ValueError("Either agent_id or agent_name must be provided")
256
+ if agent_id and agent_name:
257
+ raise ValueError("Only one of agent_id or agent_name can be provided")
258
+
259
+ existing = self.list_agents()
260
+ if agent_id:
261
+ return str(agent_id) in [str(agent.id) for agent in existing]
262
+ else:
263
+ return agent_name in [str(agent.name) for agent in existing]
264
+
265
+ def create_agent(
266
+ self,
267
+ name: Optional[str] = None,
268
+ agent_type: Optional[AgentType] = AgentType.chat_agent,
269
+ embedding_config: Optional[EmbeddingConfig] = None,
270
+ llm_config: Optional[LLMConfig] = None,
271
+ memory: Optional[Memory] = None,
272
+ block_ids: Optional[List[str]] = None,
273
+ system: Optional[str] = None,
274
+ tool_ids: Optional[List[str]] = None,
275
+ tool_rules: Optional[List[BaseToolRule]] = None,
276
+ include_base_tools: Optional[bool] = True,
277
+ include_meta_memory_tools: Optional[bool] = False,
278
+ metadata: Optional[Dict] = None,
279
+ description: Optional[str] = None,
280
+ initial_message_sequence: Optional[List[Message]] = None,
281
+ tags: Optional[List[str]] = None,
282
+ ) -> AgentState:
283
+ """Create an agent."""
284
+ request_data = {
285
+ "name": name,
286
+ "agent_type": agent_type,
287
+ "embedding_config": embedding_config.model_dump() if embedding_config else None,
288
+ "llm_config": llm_config.model_dump() if llm_config else None,
289
+ "memory": memory.model_dump() if memory else None,
290
+ "block_ids": block_ids,
291
+ "system": system,
292
+ "tool_ids": tool_ids,
293
+ "tool_rules": [rule.model_dump() if hasattr(rule, 'model_dump') else rule for rule in (tool_rules or [])],
294
+ "include_base_tools": include_base_tools,
295
+ "include_meta_memory_tools": include_meta_memory_tools,
296
+ "metadata": metadata,
297
+ "description": description,
298
+ "initial_message_sequence": [msg.model_dump() if hasattr(msg, 'model_dump') else msg for msg in (initial_message_sequence or [])],
299
+ "tags": tags,
300
+ }
301
+
302
+ data = self._request("POST", "/agents", json=request_data)
303
+ return AgentState(**data)
304
+
305
+ def update_agent(
306
+ self,
307
+ agent_id: str,
308
+ name: Optional[str] = None,
309
+ description: Optional[str] = None,
310
+ system: Optional[str] = None,
311
+ tool_ids: Optional[List[str]] = None,
312
+ metadata: Optional[Dict] = None,
313
+ llm_config: Optional[LLMConfig] = None,
314
+ embedding_config: Optional[EmbeddingConfig] = None,
315
+ message_ids: Optional[List[str]] = None,
316
+ memory: Optional[Memory] = None,
317
+ tags: Optional[List[str]] = None,
318
+ ):
319
+ """Update an agent."""
320
+ request_data = {
321
+ "name": name,
322
+ "description": description,
323
+ "system": system,
324
+ "tool_ids": tool_ids,
325
+ "metadata": metadata,
326
+ "llm_config": llm_config.model_dump() if llm_config else None,
327
+ "embedding_config": embedding_config.model_dump() if embedding_config else None,
328
+ "message_ids": message_ids,
329
+ "memory": memory.model_dump() if memory else None,
330
+ "tags": tags,
331
+ }
332
+
333
+ data = self._request("PATCH", f"/agents/{agent_id}", json=request_data)
334
+ return AgentState(**data)
335
+
336
+ def get_agent(self, agent_id: str) -> AgentState:
337
+ """Get an agent by ID."""
338
+ data = self._request("GET", f"/agents/{agent_id}")
339
+ return AgentState(**data)
340
+
341
+ def get_agent_id(self, agent_name: str) -> Optional[str]:
342
+ """Get agent ID by name."""
343
+ agents = self.list_agents()
344
+ for agent in agents:
345
+ if agent.name == agent_name:
346
+ return agent.id
347
+ return None
348
+
349
+ def delete_agent(self, agent_id: str):
350
+ """Delete an agent."""
351
+ self._request("DELETE", f"/agents/{agent_id}")
352
+
353
+ def rename_agent(self, agent_id: str, new_name: str):
354
+ """Rename an agent."""
355
+ self.update_agent(agent_id, name=new_name)
356
+
357
+ def get_tools_from_agent(self, agent_id: str) -> List[Tool]:
358
+ """Get tools from an agent."""
359
+ agent = self.get_agent(agent_id)
360
+ return agent.tools
361
+
362
+ def add_tool_to_agent(self, agent_id: str, tool_id: str):
363
+ """Add a tool to an agent."""
364
+ raise NotImplementedError("add_tool_to_agent not yet implemented in REST API")
365
+
366
+ def remove_tool_from_agent(self, agent_id: str, tool_id: str):
367
+ """Remove a tool from an agent."""
368
+ raise NotImplementedError("remove_tool_from_agent not yet implemented in REST API")
369
+
370
+ # ========================================================================
371
+ # Memory Methods
372
+ # ========================================================================
373
+
374
+ def get_in_context_memory(self, agent_id: str) -> Memory:
375
+ """Get in-context memory of an agent."""
376
+ data = self._request("GET", f"/agents/{agent_id}/memory")
377
+ return Memory(**data)
378
+
379
+ def update_in_context_memory(
380
+ self, agent_id: str, section: str, value: Union[List[str], str]
381
+ ) -> Memory:
382
+ """Update in-context memory."""
383
+ raise NotImplementedError("update_in_context_memory not yet implemented in REST API")
384
+
385
+ def get_archival_memory_summary(self, agent_id: str) -> ArchivalMemorySummary:
386
+ """Get archival memory summary."""
387
+ data = self._request("GET", f"/agents/{agent_id}/memory/archival")
388
+ return ArchivalMemorySummary(**data)
389
+
390
+ def get_recall_memory_summary(self, agent_id: str) -> RecallMemorySummary:
391
+ """Get recall memory summary."""
392
+ data = self._request("GET", f"/agents/{agent_id}/memory/recall")
393
+ return RecallMemorySummary(**data)
394
+
395
+ def get_in_context_messages(self, agent_id: str) -> List[Message]:
396
+ """Get in-context messages."""
397
+ raise NotImplementedError("get_in_context_messages not yet implemented in REST API")
398
+
399
+ # ========================================================================
400
+ # Message Methods
401
+ # ========================================================================
402
+
403
+ def send_message(
404
+ self,
405
+ message: str,
406
+ role: str,
407
+ agent_id: Optional[str] = None,
408
+ name: Optional[str] = None,
409
+ stream: Optional[bool] = False,
410
+ stream_steps: bool = False,
411
+ stream_tokens: bool = False,
412
+ filter_tags: Optional[Dict[str, Any]] = None,
413
+ use_cache: bool = True,
414
+ ) -> MirixResponse:
415
+ """Send a message to an agent.
416
+
417
+ Args:
418
+ message: The message text to send
419
+ role: The role of the message sender (user/system)
420
+ agent_id: The ID of the agent to send the message to
421
+ name: Optional name of the message sender
422
+ stream: Enable streaming (not yet implemented)
423
+ stream_steps: Stream intermediate steps
424
+ stream_tokens: Stream tokens as they are generated
425
+ filter_tags: Optional filter tags for categorization and filtering.
426
+ Example: {"project_id": "proj-alpha", "session_id": "sess-123"}
427
+ use_cache: Control Redis cache behavior (default: True)
428
+
429
+ Returns:
430
+ MirixResponse: The response from the agent
431
+
432
+ Example:
433
+ >>> response = client.send_message(
434
+ ... message="What's the status?",
435
+ ... role="user",
436
+ ... agent_id="agent123",
437
+ ... filter_tags={"project": "alpha", "priority": "high"}
438
+ ... )
439
+ """
440
+ if stream or stream_steps or stream_tokens:
441
+ raise NotImplementedError("Streaming not yet implemented in REST API")
442
+
443
+ request_data = {
444
+ "message": message,
445
+ "role": role,
446
+ "name": name,
447
+ "stream_steps": stream_steps,
448
+ "stream_tokens": stream_tokens,
449
+ }
450
+
451
+ # Include filter_tags if provided
452
+ if filter_tags is not None:
453
+ request_data["filter_tags"] = filter_tags
454
+
455
+ # Include use_cache if not default
456
+ if not use_cache:
457
+ request_data["use_cache"] = use_cache
458
+
459
+ data = self._request("POST", f"/agents/{agent_id}/messages", json=request_data)
460
+ return MirixResponse(**data)
461
+
462
+ def user_message(self, agent_id: str, message: str) -> MirixResponse:
463
+ """Send a user message to an agent."""
464
+ return self.send_message(message=message, role="user", agent_id=agent_id)
465
+
466
+ def get_messages(
467
+ self,
468
+ agent_id: str,
469
+ before: Optional[str] = None,
470
+ after: Optional[str] = None,
471
+ limit: Optional[int] = 1000,
472
+ use_cache: bool = True,
473
+ ) -> List[Message]:
474
+ """Get messages from an agent.
475
+
476
+ Args:
477
+ agent_id: The ID of the agent
478
+ before: Get messages before this cursor
479
+ after: Get messages after this cursor
480
+ limit: Maximum number of messages to retrieve
481
+ use_cache: Control Redis cache behavior (default: True)
482
+
483
+ Returns:
484
+ List of messages
485
+ """
486
+ params = {"limit": limit}
487
+ if before:
488
+ params["cursor"] = before
489
+ if not use_cache:
490
+ params["use_cache"] = "false"
491
+
492
+ data = self._request("GET", f"/agents/{agent_id}/messages", params=params)
493
+ return [Message(**msg) for msg in data]
494
+
495
+ # ========================================================================
496
+ # Tool Methods
497
+ # ========================================================================
498
+
499
+ def list_tools(
500
+ self, cursor: Optional[str] = None, limit: Optional[int] = 50
501
+ ) -> List[Tool]:
502
+ """List all tools."""
503
+ params = {"limit": limit}
504
+ if cursor:
505
+ params["cursor"] = cursor
506
+
507
+ data = self._request("GET", "/tools", params=params)
508
+ return [Tool(**tool) for tool in data]
509
+
510
+ def get_tool(self, id: str) -> Tool:
511
+ """Get a tool by ID."""
512
+ data = self._request("GET", f"/tools/{id}")
513
+ return Tool(**data)
514
+
515
+ def create_tool(
516
+ self,
517
+ func,
518
+ name: Optional[str] = None,
519
+ tags: Optional[List[str]] = None,
520
+ return_char_limit: int = FUNCTION_RETURN_CHAR_LIMIT,
521
+ ) -> Tool:
522
+ """Create a tool."""
523
+ raise NotImplementedError(
524
+ "create_tool with function not supported in MirixClient. "
525
+ "Tools must be created on the server side."
526
+ )
527
+
528
+ def create_or_update_tool(
529
+ self,
530
+ func,
531
+ name: Optional[str] = None,
532
+ tags: Optional[List[str]] = None,
533
+ return_char_limit: int = FUNCTION_RETURN_CHAR_LIMIT,
534
+ ) -> Tool:
535
+ """Create or update a tool."""
536
+ raise NotImplementedError(
537
+ "create_or_update_tool with function not supported in MirixClient. "
538
+ "Tools must be created on the server side."
539
+ )
540
+
541
+ def update_tool(
542
+ self,
543
+ id: str,
544
+ name: Optional[str] = None,
545
+ description: Optional[str] = None,
546
+ func: Optional[Callable] = None,
547
+ tags: Optional[List[str]] = None,
548
+ return_char_limit: int = FUNCTION_RETURN_CHAR_LIMIT,
549
+ ) -> Tool:
550
+ """Update a tool."""
551
+ raise NotImplementedError("update_tool not yet implemented in REST API")
552
+
553
+ def delete_tool(self, id: str):
554
+ """Delete a tool."""
555
+ self._request("DELETE", f"/tools/{id}")
556
+
557
+ def get_tool_id(self, name: str) -> Optional[str]:
558
+ """Get tool ID by name."""
559
+ tools = self.list_tools()
560
+ for tool in tools:
561
+ if tool.name == name:
562
+ return tool.id
563
+ return None
564
+
565
+ def upsert_base_tools(self) -> List[Tool]:
566
+ """Upsert base tools."""
567
+ raise NotImplementedError("upsert_base_tools must be done on server side")
568
+
569
+ # ========================================================================
570
+ # Block Methods
571
+ # ========================================================================
572
+
573
+ def list_blocks(
574
+ self, label: Optional[str] = None, templates_only: Optional[bool] = True
575
+ ) -> List[Block]:
576
+ """List blocks."""
577
+ params = {}
578
+ if label:
579
+ params["label"] = label
580
+
581
+ data = self._request("GET", "/blocks", params=params)
582
+ return [Block(**block) for block in data]
583
+
584
+ def get_block(self, block_id: str) -> Block:
585
+ """Get a block by ID."""
586
+ data = self._request("GET", f"/blocks/{block_id}")
587
+ return Block(**data)
588
+
589
+ def create_block(
590
+ self,
591
+ label: str,
592
+ value: str,
593
+ limit: Optional[int] = None,
594
+ ) -> Block:
595
+ """Create a block."""
596
+ block_data = {
597
+ "label": label,
598
+ "value": value,
599
+ "limit": limit,
600
+ }
601
+
602
+ block = Block(**block_data)
603
+ data = self._request("POST", "/blocks", json=block.model_dump())
604
+ return Block(**data)
605
+
606
+ def delete_block(self, id: str) -> Block:
607
+ """Delete a block."""
608
+ self._request("DELETE", f"/blocks/{id}")
609
+
610
+ # ========================================================================
611
+ # Human/Persona Methods
612
+ # ========================================================================
613
+
614
+ def create_human(self, name: str, text: str) -> Human:
615
+ """Create a human block."""
616
+ human = Human(value=text)
617
+ data = self._request("POST", "/blocks", json=human.model_dump())
618
+ return Human(**data)
619
+
620
+ def create_persona(self, name: str, text: str) -> Persona:
621
+ """Create a persona block."""
622
+ persona = Persona(value=text)
623
+ data = self._request("POST", "/blocks", json=persona.model_dump())
624
+ return Persona(**data)
625
+
626
+ def list_humans(self) -> List[Human]:
627
+ """List human blocks."""
628
+ blocks = self.list_blocks(label="human")
629
+ return [Human(**block.model_dump()) for block in blocks]
630
+
631
+ def list_personas(self) -> List[Persona]:
632
+ """List persona blocks."""
633
+ blocks = self.list_blocks(label="persona")
634
+ return [Persona(**block.model_dump()) for block in blocks]
635
+
636
+ def update_human(self, human_id: str, text: str) -> Human:
637
+ """Update a human block."""
638
+ raise NotImplementedError("update_human not yet implemented in REST API")
639
+
640
+ def update_persona(self, persona_id: str, text: str) -> Persona:
641
+ """Update a persona block."""
642
+ raise NotImplementedError("update_persona not yet implemented in REST API")
643
+
644
+ def get_persona(self, id: str) -> Persona:
645
+ """Get a persona block."""
646
+ data = self._request("GET", f"/blocks/{id}")
647
+ return Persona(**data)
648
+
649
+ def get_human(self, id: str) -> Human:
650
+ """Get a human block."""
651
+ data = self._request("GET", f"/blocks/{id}")
652
+ return Human(**data)
653
+
654
+ def get_persona_id(self, name: str) -> str:
655
+ """Get persona ID by name."""
656
+ personas = self.list_personas()
657
+ if personas:
658
+ return personas[0].id
659
+ return None
660
+
661
+ def get_human_id(self, name: str) -> str:
662
+ """Get human ID by name."""
663
+ humans = self.list_humans()
664
+ if humans:
665
+ return humans[0].id
666
+ return None
667
+
668
+ def delete_persona(self, id: str):
669
+ """Delete a persona."""
670
+ self.delete_block(id)
671
+
672
+ def delete_human(self, id: str):
673
+ """Delete a human."""
674
+ self.delete_block(id)
675
+
676
+ # ========================================================================
677
+ # Configuration Methods
678
+ # ========================================================================
679
+
680
+ def list_model_configs(self) -> List[LLMConfig]:
681
+ """List available LLM configurations."""
682
+ data = self._request("GET", "/config/llm")
683
+ return [LLMConfig(**config) for config in data]
684
+
685
+ def list_embedding_configs(self) -> List[EmbeddingConfig]:
686
+ """List available embedding configurations."""
687
+ data = self._request("GET", "/config/embedding")
688
+ return [EmbeddingConfig(**config) for config in data]
689
+
690
+ # ========================================================================
691
+ # Organization Methods
692
+ # ========================================================================
693
+
694
+ def create_org(self, name: Optional[str] = None) -> Organization:
695
+ """Create an organization."""
696
+ data = self._request("POST", "/organizations", json={"name": name})
697
+ return Organization(**data)
698
+
699
+ def list_orgs(
700
+ self, cursor: Optional[str] = None, limit: Optional[int] = 50
701
+ ) -> List[Organization]:
702
+ """List organizations."""
703
+ params = {"limit": limit}
704
+ if cursor:
705
+ params["cursor"] = cursor
706
+
707
+ data = self._request("GET", "/organizations", params=params)
708
+ return [Organization(**org) for org in data]
709
+
710
+ def delete_org(self, org_id: str) -> Organization:
711
+ """Delete an organization."""
712
+ raise NotImplementedError("delete_org not yet implemented in REST API")
713
+
714
+ # ========================================================================
715
+ # Sandbox Methods (Not Implemented)
716
+ # ========================================================================
717
+
718
+ def create_sandbox_config(
719
+ self, config: Union[LocalSandboxConfig, E2BSandboxConfig]
720
+ ) -> SandboxConfig:
721
+ """Create sandbox config."""
722
+ raise NotImplementedError("Sandbox config not yet implemented in REST API")
723
+
724
+ def update_sandbox_config(
725
+ self,
726
+ sandbox_config_id: str,
727
+ config: Union[LocalSandboxConfig, E2BSandboxConfig],
728
+ ) -> SandboxConfig:
729
+ """Update sandbox config."""
730
+ raise NotImplementedError("Sandbox config not yet implemented in REST API")
731
+
732
+ def delete_sandbox_config(self, sandbox_config_id: str) -> None:
733
+ """Delete sandbox config."""
734
+ raise NotImplementedError("Sandbox config not yet implemented in REST API")
735
+
736
+ def list_sandbox_configs(
737
+ self, limit: int = 50, cursor: Optional[str] = None
738
+ ) -> List[SandboxConfig]:
739
+ """List sandbox configs."""
740
+ raise NotImplementedError("Sandbox config not yet implemented in REST API")
741
+
742
+ def create_sandbox_env_var(
743
+ self,
744
+ sandbox_config_id: str,
745
+ key: str,
746
+ value: str,
747
+ description: Optional[str] = None,
748
+ ) -> SandboxEnvironmentVariable:
749
+ """Create sandbox environment variable."""
750
+ raise NotImplementedError("Sandbox env vars not yet implemented in REST API")
751
+
752
+ def update_sandbox_env_var(
753
+ self,
754
+ env_var_id: str,
755
+ key: Optional[str] = None,
756
+ value: Optional[str] = None,
757
+ description: Optional[str] = None,
758
+ ) -> SandboxEnvironmentVariable:
759
+ """Update sandbox environment variable."""
760
+ raise NotImplementedError("Sandbox env vars not yet implemented in REST API")
761
+
762
+ def delete_sandbox_env_var(self, env_var_id: str) -> None:
763
+ """Delete sandbox environment variable."""
764
+ raise NotImplementedError("Sandbox env vars not yet implemented in REST API")
765
+
766
+ def list_sandbox_env_vars(
767
+ self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
768
+ ) -> List[SandboxEnvironmentVariable]:
769
+ """List sandbox environment variables."""
770
+ raise NotImplementedError("Sandbox env vars not yet implemented in REST API")
771
+
772
+ # ========================================================================
773
+ # New Memory API Methods
774
+ # ========================================================================
775
+
776
+ def _load_system_prompts(self, config: Dict[str, Any]) -> Dict[str, str]:
777
+ """Load all system prompts from the system_prompts_folder.
778
+
779
+ Args:
780
+ config: Configuration dictionary that may contain 'system_prompts_folder'
781
+
782
+ Returns:
783
+ Dict mapping agent names to their prompt text
784
+ """
785
+ import os
786
+ import logging
787
+
788
+ logger = logging.getLogger(__name__)
789
+ prompts = {}
790
+
791
+ system_prompts_folder = config.get("system_prompts_folder")
792
+ if not system_prompts_folder:
793
+ return prompts
794
+
795
+ if not os.path.exists(system_prompts_folder):
796
+ return prompts
797
+
798
+ # Load all .txt files from the system prompts folder
799
+ for filename in os.listdir(system_prompts_folder):
800
+ if filename.endswith(".txt"):
801
+ agent_name = filename[:-4] # Strip .txt suffix
802
+ prompt_file = os.path.join(system_prompts_folder, filename)
803
+
804
+ try:
805
+ with open(prompt_file, "r", encoding="utf-8") as f:
806
+ prompts[agent_name] = f.read()
807
+ except Exception as e:
808
+ # Log warning but continue
809
+ logger.warning(
810
+ f"Failed to load system prompt for {agent_name} from {prompt_file}: {e}"
811
+ )
812
+
813
+ return prompts
814
+
815
+ def initialize_meta_agent(
816
+ self,
817
+ config: Optional[Dict[str, Any]] = None,
818
+ config_path: Optional[str] = None,
819
+ update_agents: Optional[bool] = False,
820
+ ) -> AgentState:
821
+ """
822
+ Initialize a meta agent with the given configuration.
823
+
824
+ This creates a meta memory agent that manages multiple specialized memory agents
825
+ (episodic, semantic, procedural, etc.) for the current project.
826
+
827
+ Args:
828
+ config: Configuration dictionary with llm_config, embedding_config, etc.
829
+ config_path: Path to YAML config file (alternative to config dict)
830
+
831
+ Returns:
832
+ AgentState: The initialized meta agent
833
+
834
+ Example:
835
+ >>> client = MirixClient(project="test")
836
+ >>> config = {
837
+ ... "llm_config": {"model": "gemini-2.0-flash"},
838
+ ... "embedding_config": {"model": "text-embedding-004"}
839
+ ... }
840
+ >>> meta_agent = client.initialize_meta_agent(config=config)
841
+ """
842
+ # Load config from file if provided
843
+ if config_path:
844
+ import yaml
845
+ from pathlib import Path
846
+
847
+ config_file = Path(config_path)
848
+ if config_file.exists():
849
+ with open(config_file, "r") as f:
850
+ config = yaml.safe_load(f)
851
+
852
+ if not config:
853
+ raise ValueError("Either config or config_path must be provided")
854
+
855
+ # Load system prompts from folder if specified and not already provided
856
+ if config.get("meta_agent_config") and config['meta_agent_config'].get("system_prompts_folder") and not config.get("system_prompts"):
857
+ config['meta_agent_config']["system_prompts"] = self._load_system_prompts(config['meta_agent_config'])
858
+ del config['meta_agent_config']['system_prompts_folder']
859
+
860
+ # Prepare request data
861
+ request_data = {
862
+ "config": config,
863
+ "update_agents": update_agents,
864
+ }
865
+
866
+ # Make API request to initialize meta agent
867
+ data = self._request("POST", "/agents/meta/initialize", json=request_data)
868
+ self._meta_agent = AgentState(**data)
869
+ return self._meta_agent
870
+
871
+ def add(
872
+ self,
873
+ user_id: str,
874
+ messages: List[Dict[str, Any]],
875
+ chaining: bool = True,
876
+ verbose: bool = False,
877
+ filter_tags: Optional[Dict[str, Any]] = None,
878
+ use_cache: bool = True,
879
+ ) -> Dict[str, Any]:
880
+ """
881
+ Add conversation turns to memory (asynchronous processing).
882
+
883
+ This method queues conversation turns for background processing by queue workers.
884
+ The messages are stored in the appropriate memory systems asynchronously.
885
+
886
+ Args:
887
+ user_id: User ID for the conversation
888
+ messages: List of message dicts with role and content.
889
+ Messages should end with an assistant turn.
890
+ Format: [
891
+ {"role": "user", "content": [{"type": "text", "text": "..."}]},
892
+ {"role": "assistant", "content": [{"type": "text", "text": "..."}]}
893
+ ]
894
+ verbose: If True, enable verbose output during memory processing
895
+ filter_tags: Optional dict of tags for filtering and categorization.
896
+ Example: {"project_id": "proj-123", "session_id": "sess-456"}
897
+ use_cache: Control Redis cache behavior (default: True)
898
+
899
+ Returns:
900
+ Dict containing:
901
+ - success (bool): True if message was queued successfully
902
+ - message (str): Status message
903
+ - status (str): "queued" - indicates async processing
904
+ - agent_id (str): Meta agent ID processing the messages
905
+ - message_count (int): Number of messages queued
906
+
907
+ Note:
908
+ Processing happens asynchronously. The response indicates the message
909
+ was successfully queued, not that processing is complete.
910
+
911
+ Example:
912
+ >>> response = client.add(
913
+ ... user_id='user_123',
914
+ ... messages=[
915
+ ... {"role": "user", "content": [{"type": "text", "text": "I went to dinner"}]},
916
+ ... {"role": "assistant", "content": [{"type": "text", "text": "That's great!"}]}
917
+ ... ],
918
+ ... verbose=True,
919
+ ... filter_tags={"session_id": "sess-789", "tags": ["personal"]}
920
+ ... )
921
+ >>> logger.debug(response)
922
+ {
923
+ "success": True,
924
+ "message": "Memory queued for processing",
925
+ "status": "queued",
926
+ "agent_id": "agent-456",
927
+ "message_count": 2
928
+ }
929
+ """
930
+ if not self._meta_agent:
931
+ raise ValueError("Meta agent not initialized. Call initialize_meta_agent() first.")
932
+
933
+ request_data = {
934
+ "user_id": user_id,
935
+ "meta_agent_id": self._meta_agent.id,
936
+ "messages": messages,
937
+ "chaining": chaining,
938
+ "verbose": verbose,
939
+ }
940
+
941
+ if filter_tags is not None:
942
+ request_data["filter_tags"] = filter_tags
943
+
944
+ if not use_cache:
945
+ request_data["use_cache"] = use_cache
946
+
947
+ return self._request("POST", "/memory/add", json=request_data)
948
+
949
+ def retrieve_with_conversation(
950
+ self,
951
+ user_id: str,
952
+ messages: List[Dict[str, Any]],
953
+ limit: int = 10,
954
+ filter_tags: Optional[Dict[str, Any]] = None,
955
+ use_cache: bool = True,
956
+ ) -> Dict[str, Any]:
957
+ """
958
+ Retrieve relevant memories based on conversation context.
959
+
960
+ This method analyzes the conversation and retrieves relevant memories
961
+ from all memory systems.
962
+
963
+ Args:
964
+ user_id: User ID for the conversation
965
+ messages: List of message dicts with role and content.
966
+ Messages should end with a user turn.
967
+ Format: [
968
+ {"role": "user", "content": [{"type": "text", "text": "..."}]}
969
+ ]
970
+ limit: Maximum number of items to retrieve per memory type (default: 10)
971
+ filter_tags: Optional dict of tags for filtering results.
972
+ Only memories matching these tags will be returned.
973
+ use_cache: Control Redis cache behavior (default: True)
974
+
975
+ Returns:
976
+ Dict containing retrieved memories organized by type
977
+
978
+ Example:
979
+ >>> memories = client.retrieve_with_conversation(
980
+ ... user_id='user_123',
981
+ ... messages=[
982
+ ... {"role": "user", "content": [{"type": "text", "text": "Where did I go yesterday?"}]}
983
+ ... ],
984
+ ... limit=5,
985
+ ... filter_tags={"session_id": "sess-789"}
986
+ ... )
987
+ """
988
+ if not self._meta_agent:
989
+ raise ValueError("Meta agent not initialized. Call initialize_meta_agent() first.")
990
+
991
+ request_data = {
992
+ "user_id": user_id,
993
+ "messages": messages,
994
+ "limit": limit,
995
+ }
996
+
997
+ if filter_tags is not None:
998
+ request_data["filter_tags"] = filter_tags
999
+
1000
+ if not use_cache:
1001
+ request_data["use_cache"] = use_cache
1002
+
1003
+ return self._request("POST", "/memory/retrieve/conversation", json=request_data)
1004
+
1005
+ def retrieve_with_topic(
1006
+ self,
1007
+ user_id: str,
1008
+ topic: str,
1009
+ limit: int = 10,
1010
+ ) -> Dict[str, Any]:
1011
+ """
1012
+ Retrieve relevant memories based on a topic.
1013
+
1014
+ This method searches for memories related to a specific topic or keyword.
1015
+
1016
+ Args:
1017
+ user_id: User ID for the conversation
1018
+ topic: Topic or keyword to search for
1019
+ limit: Maximum number of items to retrieve per memory type (default: 10)
1020
+
1021
+ Returns:
1022
+ Dict containing retrieved memories organized by type
1023
+
1024
+ Example:
1025
+ >>> memories = client.retrieve_with_topic(
1026
+ ... user_id='user_123',
1027
+ ... topic="dinner",
1028
+ ... limit=5
1029
+ ... )
1030
+ """
1031
+ if not self._meta_agent:
1032
+ raise ValueError("Meta agent not initialized. Call initialize_meta_agent() first.")
1033
+
1034
+ params = {
1035
+ "user_id": user_id,
1036
+ "topic": topic,
1037
+ "limit": limit,
1038
+ }
1039
+
1040
+ return self._request("GET", "/memory/retrieve/topic", params=params)
1041
+
1042
+ def search(
1043
+ self,
1044
+ user_id: str,
1045
+ query: str,
1046
+ memory_type: str = "all",
1047
+ search_field: str = "null",
1048
+ search_method: str = "bm25",
1049
+ limit: int = 10,
1050
+ ) -> Dict[str, Any]:
1051
+ """
1052
+ Search for memories using various search methods.
1053
+ Similar to the search_in_memory tool function.
1054
+
1055
+ This method performs a search across specified memory types and returns
1056
+ a flat list of results.
1057
+
1058
+ Args:
1059
+ user_id: User ID for the conversation
1060
+ query: Search query
1061
+ memory_type: Type of memory to search. Options: "episodic", "resource",
1062
+ "procedural", "knowledge_vault", "semantic", "all" (default: "all")
1063
+ search_field: Field to search in. Options vary by memory type:
1064
+ - episodic: "summary", "details"
1065
+ - resource: "summary", "content"
1066
+ - procedural: "summary", "steps"
1067
+ - knowledge_vault: "caption", "secret_value"
1068
+ - semantic: "name", "summary", "details"
1069
+ - For "all": use "null" (default)
1070
+ search_method: Search method. Options: "bm25" (default), "embedding"
1071
+ limit: Maximum number of results per memory type (default: 10)
1072
+
1073
+ Returns:
1074
+ Dict containing:
1075
+ - success: bool
1076
+ - query: str (the search query)
1077
+ - memory_type: str (the memory type searched)
1078
+ - search_field: str (the field searched)
1079
+ - search_method: str (the search method used)
1080
+ - results: List[Dict] (flat list of results from all memory types)
1081
+ - count: int (total number of results)
1082
+
1083
+ Example:
1084
+ >>> # Search all memory types
1085
+ >>> results = client.search(
1086
+ ... user_id='user_123',
1087
+ ... query="restaurants",
1088
+ ... limit=5
1089
+ ... )
1090
+ logger.debug("Found %s results", results['count'])
1091
+ >>>
1092
+ >>> # Search only episodic memories in details field
1093
+ >>> episodic_results = client.search(
1094
+ ... user_id='user_123',
1095
+ ... query="meeting",
1096
+ ... memory_type="episodic",
1097
+ ... search_field="details",
1098
+ ... limit=10
1099
+ ... )
1100
+ """
1101
+ if not self._meta_agent:
1102
+ raise ValueError("Meta agent not initialized. Call initialize_meta_agent() first.")
1103
+
1104
+ params = {
1105
+ "user_id": user_id,
1106
+ "query": query,
1107
+ "memory_type": memory_type,
1108
+ "search_field": search_field,
1109
+ "search_method": search_method,
1110
+ "limit": limit,
1111
+ }
1112
+
1113
+ return self._request("GET", "/memory/search", params=params)
1114
+
1115
+ # ========================================================================
1116
+ # LangChain/Composio/CrewAI Integration (Not Supported)
1117
+ # ========================================================================
1118
+
1119
+ def load_langchain_tool(
1120
+ self,
1121
+ langchain_tool: "LangChainBaseTool",
1122
+ additional_imports_module_attr_map: dict[str, str] = None,
1123
+ ) -> Tool:
1124
+ """Load LangChain tool."""
1125
+ raise NotImplementedError(
1126
+ "load_langchain_tool not supported in MirixClient. "
1127
+ "Tools must be created on the server side."
1128
+ )
1129
+
1130
+ def load_composio_tool(self, action: "ActionType") -> Tool:
1131
+ """Load Composio tool."""
1132
+ raise NotImplementedError(
1133
+ "load_composio_tool not supported in MirixClient. "
1134
+ "Tools must be created on the server side."
1135
+ )
1136
+