memra 0.2.1__py3-none-any.whl → 0.2.2__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.
- memra/__init__.py +1 -1
- memra/execution.py +28 -8
- memra/models.py +1 -0
- memra/tool_registry.py +125 -6
- {memra-0.2.1.dist-info → memra-0.2.2.dist-info}/METADATA +29 -11
- memra-0.2.2.dist-info/RECORD +13 -0
- memra-0.2.1.dist-info/RECORD +0 -13
- {memra-0.2.1.dist-info → memra-0.2.2.dist-info}/LICENSE +0 -0
- {memra-0.2.1.dist-info → memra-0.2.2.dist-info}/WHEEL +0 -0
- {memra-0.2.1.dist-info → memra-0.2.2.dist-info}/entry_points.txt +0 -0
- {memra-0.2.1.dist-info → memra-0.2.2.dist-info}/top_level.txt +0 -0
memra/__init__.py
CHANGED
memra/execution.py
CHANGED
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
from typing import Dict, Any, List, Optional
|
4
4
|
from .models import Department, Agent, DepartmentResult, ExecutionTrace, DepartmentAudit
|
5
5
|
from .tool_registry import ToolRegistry
|
6
|
+
from .tool_registry_client import ToolRegistryClient
|
6
7
|
|
7
8
|
logger = logging.getLogger(__name__)
|
8
9
|
|
@@ -11,6 +12,7 @@ class ExecutionEngine:
|
|
11
12
|
|
12
13
|
def __init__(self):
|
13
14
|
self.tool_registry = ToolRegistry()
|
15
|
+
self.api_client = ToolRegistryClient()
|
14
16
|
self.last_execution_audit: Optional[DepartmentAudit] = None
|
15
17
|
|
16
18
|
def execute_department(self, department: Department, input_data: Dict[str, Any]) -> DepartmentResult:
|
@@ -199,12 +201,28 @@ class ExecutionEngine:
|
|
199
201
|
trace.tools_invoked.append(tool_name)
|
200
202
|
|
201
203
|
# Get tool from registry and execute
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
204
|
+
print(f"🔍 {agent.role}: Tool {tool_name} is hosted by: {hosted_by}")
|
205
|
+
if hosted_by == "memra":
|
206
|
+
# Use API client for server-hosted tools
|
207
|
+
print(f"🌐 {agent.role}: Using API client for {tool_name}")
|
208
|
+
config_to_pass = tool_spec.get("config") if isinstance(tool_spec, dict) else tool_spec.config
|
209
|
+
tool_result = self.api_client.execute_tool(
|
210
|
+
tool_name,
|
211
|
+
hosted_by,
|
212
|
+
agent_input,
|
213
|
+
config_to_pass
|
214
|
+
)
|
215
|
+
else:
|
216
|
+
# Use local registry for MCP and other local tools
|
217
|
+
print(f"🏠 {agent.role}: Using local registry for {tool_name}")
|
218
|
+
config_to_pass = tool_spec.get("config") if isinstance(tool_spec, dict) else tool_spec.config
|
219
|
+
print(f"🔧 {agent.role}: Config for {tool_name}: {config_to_pass}")
|
220
|
+
tool_result = self.tool_registry.execute_tool(
|
221
|
+
tool_name,
|
222
|
+
hosted_by,
|
223
|
+
agent_input,
|
224
|
+
config_to_pass
|
225
|
+
)
|
208
226
|
|
209
227
|
if not tool_result.get("success", False):
|
210
228
|
print(f"😟 {agent.role}: Oh no! Tool {tool_name} failed: {tool_result.get('error', 'Unknown error')}")
|
@@ -292,7 +310,8 @@ class ExecutionEngine:
|
|
292
310
|
isinstance(tool_data["validation_errors"], list) and
|
293
311
|
"is_valid" in tool_data and
|
294
312
|
# Check if it's validating real extracted data (not just mock data)
|
295
|
-
len(str(tool_data)) > 100 # Real validation results are more substantial
|
313
|
+
len(str(tool_data)) > 100 and # Real validation results are more substantial
|
314
|
+
not tool_data.get("_mock", False) # Not mock data
|
296
315
|
)
|
297
316
|
|
298
317
|
elif tool_name == "PostgresInsert":
|
@@ -302,7 +321,8 @@ class ExecutionEngine:
|
|
302
321
|
tool_data["success"] == True and
|
303
322
|
"record_id" in tool_data and
|
304
323
|
isinstance(tool_data["record_id"], int) and # Real DB returns integer IDs
|
305
|
-
"database_table" in tool_data # Real implementation includes table name
|
324
|
+
"database_table" in tool_data and # Real implementation includes table name
|
325
|
+
not tool_data.get("_mock", False) # Not mock data
|
306
326
|
)
|
307
327
|
|
308
328
|
# Default to mock work
|
memra/models.py
CHANGED
@@ -12,6 +12,7 @@ class Tool(BaseModel):
|
|
12
12
|
hosted_by: str = "memra" # or "mcp" for customer's Model Context Protocol
|
13
13
|
description: Optional[str] = None
|
14
14
|
parameters: Optional[Dict[str, Any]] = None
|
15
|
+
config: Optional[Dict[str, Any]] = None
|
15
16
|
|
16
17
|
class Agent(BaseModel):
|
17
18
|
role: str
|
memra/tool_registry.py
CHANGED
@@ -2,6 +2,7 @@ import importlib
|
|
2
2
|
import logging
|
3
3
|
import sys
|
4
4
|
import os
|
5
|
+
import httpx
|
5
6
|
from typing import Dict, Any, List, Optional, Callable
|
6
7
|
from pathlib import Path
|
7
8
|
|
@@ -62,9 +63,127 @@ class ToolRegistry:
|
|
62
63
|
|
63
64
|
def execute_tool(self, tool_name: str, hosted_by: str, input_data: Dict[str, Any],
|
64
65
|
config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
65
|
-
"""Execute a tool -
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
"
|
70
|
-
|
66
|
+
"""Execute a tool - handles MCP tools via bridge, rejects direct server tool execution"""
|
67
|
+
if hosted_by == "mcp":
|
68
|
+
return self._execute_mcp_tool(tool_name, input_data, config)
|
69
|
+
else:
|
70
|
+
logger.warning(f"Direct tool execution attempted for {tool_name}. Use API client instead.")
|
71
|
+
return {
|
72
|
+
"success": False,
|
73
|
+
"error": "Direct tool execution not supported. Use API client for tool execution."
|
74
|
+
}
|
75
|
+
|
76
|
+
def _execute_mcp_tool(self, tool_name: str, input_data: Dict[str, Any],
|
77
|
+
config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
78
|
+
"""Execute an MCP tool via the bridge"""
|
79
|
+
try:
|
80
|
+
# Debug logging
|
81
|
+
logger.info(f"Executing MCP tool {tool_name} with config: {config}")
|
82
|
+
|
83
|
+
# Get bridge configuration
|
84
|
+
if not config:
|
85
|
+
logger.error(f"MCP tool {tool_name} requires bridge configuration")
|
86
|
+
return {
|
87
|
+
"success": False,
|
88
|
+
"error": "MCP bridge configuration required"
|
89
|
+
}
|
90
|
+
|
91
|
+
bridge_url = config.get("bridge_url", "http://localhost:8081")
|
92
|
+
bridge_secret = config.get("bridge_secret")
|
93
|
+
|
94
|
+
if not bridge_secret:
|
95
|
+
logger.error(f"MCP tool {tool_name} requires bridge_secret in config")
|
96
|
+
return {
|
97
|
+
"success": False,
|
98
|
+
"error": "MCP bridge secret required"
|
99
|
+
}
|
100
|
+
|
101
|
+
# Try different endpoint patterns that might exist
|
102
|
+
endpoints_to_try = [
|
103
|
+
f"{bridge_url}/execute_tool",
|
104
|
+
f"{bridge_url}/tool/{tool_name}",
|
105
|
+
f"{bridge_url}/mcp/execute",
|
106
|
+
f"{bridge_url}/api/execute"
|
107
|
+
]
|
108
|
+
|
109
|
+
# Prepare request
|
110
|
+
payload = {
|
111
|
+
"tool_name": tool_name,
|
112
|
+
"input_data": input_data
|
113
|
+
}
|
114
|
+
|
115
|
+
headers = {
|
116
|
+
"Content-Type": "application/json",
|
117
|
+
"X-Bridge-Secret": bridge_secret
|
118
|
+
}
|
119
|
+
|
120
|
+
# Try each endpoint
|
121
|
+
logger.info(f"Executing MCP tool {tool_name} via bridge at {bridge_url}")
|
122
|
+
|
123
|
+
last_error = None
|
124
|
+
for endpoint in endpoints_to_try:
|
125
|
+
try:
|
126
|
+
with httpx.Client(timeout=60.0) as client:
|
127
|
+
response = client.post(endpoint, json=payload, headers=headers)
|
128
|
+
|
129
|
+
if response.status_code == 200:
|
130
|
+
result = response.json()
|
131
|
+
logger.info(f"MCP tool {tool_name} executed successfully via {endpoint}")
|
132
|
+
return result
|
133
|
+
elif response.status_code == 404:
|
134
|
+
continue # Try next endpoint
|
135
|
+
else:
|
136
|
+
response.raise_for_status()
|
137
|
+
|
138
|
+
except httpx.HTTPStatusError as e:
|
139
|
+
if e.response.status_code == 404:
|
140
|
+
continue # Try next endpoint
|
141
|
+
last_error = e
|
142
|
+
continue
|
143
|
+
except Exception as e:
|
144
|
+
last_error = e
|
145
|
+
continue
|
146
|
+
|
147
|
+
# If we get here, none of the endpoints worked
|
148
|
+
# For now, return mock data to keep the workflow working
|
149
|
+
logger.warning(f"MCP bridge endpoints not available, returning mock data for {tool_name}")
|
150
|
+
|
151
|
+
if tool_name == "DataValidator":
|
152
|
+
return {
|
153
|
+
"success": True,
|
154
|
+
"data": {
|
155
|
+
"is_valid": True,
|
156
|
+
"validation_errors": [],
|
157
|
+
"validated_data": input_data.get("invoice_data", {}),
|
158
|
+
"_mock": True
|
159
|
+
}
|
160
|
+
}
|
161
|
+
elif tool_name == "PostgresInsert":
|
162
|
+
return {
|
163
|
+
"success": True,
|
164
|
+
"data": {
|
165
|
+
"success": True,
|
166
|
+
"record_id": 999, # Mock ID
|
167
|
+
"database_table": "invoices",
|
168
|
+
"inserted_data": input_data.get("invoice_data", {}),
|
169
|
+
"_mock": True
|
170
|
+
}
|
171
|
+
}
|
172
|
+
else:
|
173
|
+
return {
|
174
|
+
"success": False,
|
175
|
+
"error": f"MCP bridge not available and no mock data for {tool_name}"
|
176
|
+
}
|
177
|
+
|
178
|
+
except httpx.TimeoutException:
|
179
|
+
logger.error(f"MCP tool {tool_name} execution timed out")
|
180
|
+
return {
|
181
|
+
"success": False,
|
182
|
+
"error": f"MCP tool execution timed out after 60 seconds"
|
183
|
+
}
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"MCP tool execution failed for {tool_name}: {str(e)}")
|
186
|
+
return {
|
187
|
+
"success": False,
|
188
|
+
"error": str(e)
|
189
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: memra
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: Declarative framework for enterprise workflows with MCP integration - Client SDK
|
5
5
|
Home-page: https://github.com/memra/memra-sdk
|
6
6
|
Author: Memra
|
@@ -99,6 +99,16 @@ echo 'export MEMRA_API_KEY="your-api-key-here"' >> ~/.zshrc
|
|
99
99
|
python examples/accounts_payable_client.py
|
100
100
|
```
|
101
101
|
|
102
|
+
## Architecture
|
103
|
+
|
104
|
+
The Memra platform consists of three main components:
|
105
|
+
|
106
|
+
- **Memra SDK** (this repository): Client library for building and executing workflows
|
107
|
+
- **Memra Server**: Hosted infrastructure for heavy AI processing tools
|
108
|
+
- **MCP Bridge**: Local execution environment for database operations
|
109
|
+
|
110
|
+
Tools are automatically routed between server and local execution based on their `hosted_by` configuration.
|
111
|
+
|
102
112
|
## Documentation
|
103
113
|
|
104
114
|
Documentation is coming soon. For now, see the examples below and in the `examples/` directory.
|
@@ -115,16 +125,24 @@ We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) f
|
|
115
125
|
|
116
126
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
117
127
|
|
118
|
-
##
|
128
|
+
## Repository Structure
|
119
129
|
|
120
130
|
```
|
121
|
-
├── examples/
|
122
|
-
│ ├── accounts_payable_client.py # API-based
|
123
|
-
│ ├──
|
124
|
-
│ ├── invoice_processing.py # Simple
|
125
|
-
│ └── propane_delivery.py #
|
126
|
-
├── memra/
|
127
|
-
├──
|
128
|
-
├──
|
129
|
-
└──
|
131
|
+
├── examples/ # Example workflows and use cases
|
132
|
+
│ ├── accounts_payable_client.py # API-based accounts payable workflow
|
133
|
+
│ ├── accounts_payable_mcp.py # MCP-enabled accounts payable workflow
|
134
|
+
│ ├── invoice_processing.py # Simple invoice processing example
|
135
|
+
│ └── propane_delivery.py # Propane delivery domain example
|
136
|
+
├── memra/ # Core SDK package
|
137
|
+
│ ├── __init__.py # Package initialization
|
138
|
+
│ ├── tool_registry.py # Tool discovery and routing
|
139
|
+
│ └── sdk/ # SDK components
|
140
|
+
│ ├── __init__.py
|
141
|
+
│ ├── client.py # API client
|
142
|
+
│ ├── execution_engine.py # Workflow execution
|
143
|
+
│ └── models.py # Core data models
|
144
|
+
├── docs/ # Documentation
|
145
|
+
├── tests/ # Test suite
|
146
|
+
├── local/dependencies/ # Local development setup
|
147
|
+
└── scripts/ # Utility scripts
|
130
148
|
```
|
@@ -0,0 +1,13 @@
|
|
1
|
+
memra/__init__.py,sha256=5WPh9vku8_ZV4T6WayAqArKAj1RDkbL47SsnA9GWD7A,662
|
2
|
+
memra/discovery.py,sha256=yJIQnrDQu1nyzKykCIuzG_5SW5dIXHCEBLLKRWacIoY,480
|
3
|
+
memra/discovery_client.py,sha256=AbnKn6qhyrf7vmOvknEeDzH4tiGHsqPHtDaein_qaW0,1271
|
4
|
+
memra/execution.py,sha256=bg822ED6yYN7APjPac1LRhv48gtxV4DUPvzpyLyBa2I,21443
|
5
|
+
memra/models.py,sha256=sXMPRnMB_mUVtJdBFyd0ElCf_uh1yqx7iLssIYNm0vI,3333
|
6
|
+
memra/tool_registry.py,sha256=N7kpYQxgJcSMDDCX-_6og1-of3QKEaoz6H16ptCCg48,7784
|
7
|
+
memra/tool_registry_client.py,sha256=uzMQ4COvRams9vuPLcqcdljUpDlAYU_tyFxrRhrA0Lc,4009
|
8
|
+
memra-0.2.2.dist-info/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
memra-0.2.2.dist-info/METADATA,sha256=jrZ9AwcGtPK-pc9TGGGoi1oJzXxcGx7LUsbsd6NxcxA,4856
|
10
|
+
memra-0.2.2.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
11
|
+
memra-0.2.2.dist-info/entry_points.txt,sha256=LBVjwWoxWJRzNLgeByPn6xUvWFIRnqnemvAZgIoSt08,41
|
12
|
+
memra-0.2.2.dist-info/top_level.txt,sha256=pXWcTRS1zctdiSUivW4iyKpJ4tcfIu-1BW_fpbal3OY,6
|
13
|
+
memra-0.2.2.dist-info/RECORD,,
|
memra-0.2.1.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
memra/__init__.py,sha256=K3jA34FSI9LuDHAPyMFpG3cbX0pFVsTP2N5xzbUffiI,662
|
2
|
-
memra/discovery.py,sha256=yJIQnrDQu1nyzKykCIuzG_5SW5dIXHCEBLLKRWacIoY,480
|
3
|
-
memra/discovery_client.py,sha256=AbnKn6qhyrf7vmOvknEeDzH4tiGHsqPHtDaein_qaW0,1271
|
4
|
-
memra/execution.py,sha256=3UIP69x2Ba89vv7OQ3yAzlnl1lphGagFPgKUrqcqElk,20172
|
5
|
-
memra/models.py,sha256=nTaYLAp0tRzQ0CQaBLNBURfhBQ5_gyty0ams4mghyIc,3289
|
6
|
-
memra/tool_registry.py,sha256=vnsuH5q20AMXADNl3-7HCD26x1zHc67waxxqv_Ta6Ak,2951
|
7
|
-
memra/tool_registry_client.py,sha256=uzMQ4COvRams9vuPLcqcdljUpDlAYU_tyFxrRhrA0Lc,4009
|
8
|
-
memra-0.2.1.dist-info/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
-
memra-0.2.1.dist-info/METADATA,sha256=LI5-Dte9XuNjsnM1KVs8Xr998nViC6jmI2S1nY37lkQ,3794
|
10
|
-
memra-0.2.1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
11
|
-
memra-0.2.1.dist-info/entry_points.txt,sha256=LBVjwWoxWJRzNLgeByPn6xUvWFIRnqnemvAZgIoSt08,41
|
12
|
-
memra-0.2.1.dist-info/top_level.txt,sha256=pXWcTRS1zctdiSUivW4iyKpJ4tcfIu-1BW_fpbal3OY,6
|
13
|
-
memra-0.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|