memra 0.2.4__py3-none-any.whl → 0.2.6__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 +16 -2
- memra/cli.py +315 -0
- memra/execution.py +217 -46
- memra/models.py +1 -0
- memra/tool_registry_client.py +2 -2
- memra-0.2.6.dist-info/METADATA +319 -0
- {memra-0.2.4.dist-info → memra-0.2.6.dist-info}/RECORD +24 -14
- memra-0.2.6.dist-info/licenses/LICENSE +21 -0
- memra-ops/app.py +98 -0
- memra-ops/logic/__init__.py +1 -0
- memra-ops/logic/file_tools.py +43 -0
- memra-ops/logic/invoice_tools.py +668 -0
- memra-ops/logic/invoice_tools_fix.py +66 -0
- memra-ops/mcp_bridge_server.py +1178 -0
- memra-ops/scripts/check_database.py +37 -0
- memra-ops/scripts/clear_database.py +48 -0
- memra-ops/scripts/monitor_database.py +67 -0
- memra-ops/scripts/reset_database.py +65 -0
- memra-ops/server_tool_registry.py +3 -1
- memra-sdk/memra/__init__.py +1 -1
- memra-sdk/setup.py +1 -1
- memra-0.2.4.dist-info/METADATA +0 -145
- memra-0.2.4.dist-info/licenses/LICENSE +0 -0
- {memra-0.2.4.dist-info → memra-0.2.6.dist-info}/WHEEL +0 -0
- {memra-0.2.4.dist-info → memra-0.2.6.dist-info}/entry_points.txt +0 -0
- {memra-0.2.4.dist-info → memra-0.2.6.dist-info}/top_level.txt +0 -0
memra/execution.py
CHANGED
@@ -83,7 +83,45 @@ class ExecutionEngine:
|
|
83
83
|
)
|
84
84
|
|
85
85
|
# Store result for next agent
|
86
|
-
|
86
|
+
agent_result_data = result.get("data")
|
87
|
+
|
88
|
+
# DEBUG: Log what each agent is actually outputting
|
89
|
+
print(f"🔍 DEBUG: {agent.role} output_key='{agent.output_key}'")
|
90
|
+
print(f"🔍 DEBUG: {agent.role} result_data type: {type(agent_result_data)}")
|
91
|
+
if isinstance(agent_result_data, dict):
|
92
|
+
print(f"🔍 DEBUG: {agent.role} result_data keys: {list(agent_result_data.keys())}")
|
93
|
+
else:
|
94
|
+
print(f"🔍 DEBUG: {agent.role} result_data: {agent_result_data}")
|
95
|
+
|
96
|
+
# Special handling for Invoice Parser - extract only the extracted_data
|
97
|
+
if agent.role == "Invoice Parser" and agent.output_key == "invoice_data":
|
98
|
+
# PDFProcessor returns: {'success': True, 'data': {'extracted_data': {...}}, '_memra_metadata': {...}}
|
99
|
+
# We need to extract: agent_result_data['data']['extracted_data']
|
100
|
+
if (isinstance(agent_result_data, dict) and
|
101
|
+
agent_result_data.get('success') and
|
102
|
+
'data' in agent_result_data and
|
103
|
+
isinstance(agent_result_data['data'], dict) and
|
104
|
+
'extracted_data' in agent_result_data['data']):
|
105
|
+
|
106
|
+
# Extract only the extracted_data portion from the nested structure
|
107
|
+
context["results"][agent.output_key] = agent_result_data['data']['extracted_data']
|
108
|
+
print(f"🔧 {agent.role}: Extracted invoice_data from nested response structure")
|
109
|
+
print(f"🔧 {agent.role}: Invoice data keys: {list(agent_result_data['data']['extracted_data'].keys())}")
|
110
|
+
else:
|
111
|
+
context["results"][agent.output_key] = agent_result_data
|
112
|
+
print(f"⚠️ {agent.role}: No extracted_data found in response")
|
113
|
+
print(f"⚠️ {agent.role}: Available keys: {list(agent_result_data.keys()) if isinstance(agent_result_data, dict) else 'not a dict'}")
|
114
|
+
else:
|
115
|
+
context["results"][agent.output_key] = agent_result_data
|
116
|
+
|
117
|
+
# DEBUG: Log what's now stored in context for next agents
|
118
|
+
print(f"🔍 DEBUG: Context now contains: {list(context['results'].keys())}")
|
119
|
+
for key, value in context["results"].items():
|
120
|
+
if isinstance(value, dict):
|
121
|
+
print(f"🔍 DEBUG: Context[{key}] keys: {list(value.keys())}")
|
122
|
+
else:
|
123
|
+
print(f"🔍 DEBUG: Context[{key}]: {value}")
|
124
|
+
|
87
125
|
print(f"✅ Step {i} completed in {agent_duration:.1f}s")
|
88
126
|
|
89
127
|
# Execute manager agent for final validation if present
|
@@ -170,12 +208,19 @@ class ExecutionEngine:
|
|
170
208
|
|
171
209
|
# Prepare input data for agent
|
172
210
|
agent_input = {}
|
211
|
+
print(f"🔍 DEBUG: {agent.role} input_keys: {agent.input_keys}")
|
212
|
+
print(f"🔍 DEBUG: {agent.role} context input keys: {list(context['input'].keys())}")
|
213
|
+
print(f"🔍 DEBUG: {agent.role} context results keys: {list(context['results'].keys())}")
|
214
|
+
|
173
215
|
for key in agent.input_keys:
|
174
216
|
if key in context["input"]:
|
175
217
|
agent_input[key] = context["input"][key]
|
176
218
|
print(f"📥 {agent.role}: I received '{key}' as input")
|
177
219
|
elif key in context["results"]:
|
178
|
-
|
220
|
+
# Handle data transformation for specific tools
|
221
|
+
raw_data = context["results"][key]
|
222
|
+
|
223
|
+
agent_input[key] = raw_data
|
179
224
|
print(f"📥 {agent.role}: I got '{key}' from a previous agent")
|
180
225
|
else:
|
181
226
|
print(f"🤔 {agent.role}: Hmm, I'm missing input '{key}' but I'll try to work without it")
|
@@ -246,6 +291,99 @@ class ExecutionEngine:
|
|
246
291
|
"error": f"Tool {tool_name} failed: {tool_result.get('error', 'Unknown error')}"
|
247
292
|
}
|
248
293
|
|
294
|
+
# Print JSON data for vision model tools
|
295
|
+
if tool_name in ["PDFProcessor", "InvoiceExtractionWorkflow"]:
|
296
|
+
print(f"\n🔍 {agent.role}: VISION MODEL JSON DATA - {tool_name}")
|
297
|
+
print("=" * 60)
|
298
|
+
print(f"📊 Tool: {tool_name}")
|
299
|
+
print(f"✅ Success: {tool_result.get('success', 'Unknown')}")
|
300
|
+
|
301
|
+
# Handle nested data structure
|
302
|
+
nested_data = tool_result.get('data', {})
|
303
|
+
if 'data' in nested_data:
|
304
|
+
nested_data = nested_data['data']
|
305
|
+
|
306
|
+
print(f"📄 Data Structure:")
|
307
|
+
print(f" - Keys: {list(nested_data.keys())}")
|
308
|
+
|
309
|
+
# Print extracted text if available
|
310
|
+
if 'extracted_text' in nested_data:
|
311
|
+
text = nested_data['extracted_text']
|
312
|
+
print(f"📝 Extracted Text ({len(text)} chars):")
|
313
|
+
print(f" {text[:300]}{'...' if len(text) > 300 else ''}")
|
314
|
+
else:
|
315
|
+
print("❌ No 'extracted_text' in response")
|
316
|
+
|
317
|
+
# Print extracted data if available
|
318
|
+
if 'extracted_data' in nested_data:
|
319
|
+
extracted = nested_data['extracted_data']
|
320
|
+
print(f"🎯 Extracted Data:")
|
321
|
+
for k, v in extracted.items():
|
322
|
+
print(f" {k}: {v}")
|
323
|
+
else:
|
324
|
+
print("❌ No 'extracted_data' in response")
|
325
|
+
|
326
|
+
# Print screenshot info if available
|
327
|
+
if 'screenshots_dir' in nested_data:
|
328
|
+
print(f"📸 Screenshots:")
|
329
|
+
print(f" Directory: {nested_data.get('screenshots_dir', 'N/A')}")
|
330
|
+
print(f" Count: {nested_data.get('screenshot_count', 'N/A')}")
|
331
|
+
print(f" Invoice ID: {nested_data.get('invoice_id', 'N/A')}")
|
332
|
+
|
333
|
+
if 'error' in tool_result:
|
334
|
+
print(f"❌ Error: {tool_result['error']}")
|
335
|
+
print("=" * 60)
|
336
|
+
|
337
|
+
# Print JSON data for database tools
|
338
|
+
if tool_name in ["DataValidator", "PostgresInsert"]:
|
339
|
+
print(f"\n💾 {agent.role}: DATABASE TOOL JSON DATA - {tool_name}")
|
340
|
+
print("=" * 60)
|
341
|
+
print(f"📊 Tool: {tool_name}")
|
342
|
+
print(f"✅ Success: {tool_result.get('success', 'Unknown')}")
|
343
|
+
|
344
|
+
if 'data' in tool_result:
|
345
|
+
data = tool_result['data']
|
346
|
+
print(f"📄 Data Structure:")
|
347
|
+
print(f" - Keys: {list(data.keys())}")
|
348
|
+
|
349
|
+
# Print validation results
|
350
|
+
if tool_name == "DataValidator":
|
351
|
+
print(f"🔍 Validation Results:")
|
352
|
+
print(f" Valid: {data.get('is_valid', 'N/A')}")
|
353
|
+
print(f" Errors: {data.get('validation_errors', 'N/A')}")
|
354
|
+
if 'validated_data' in data:
|
355
|
+
validated = data['validated_data']
|
356
|
+
if isinstance(validated, dict) and 'extracted_data' in validated:
|
357
|
+
extracted = validated['extracted_data']
|
358
|
+
print(f" Data to Insert:")
|
359
|
+
print(f" Vendor: '{extracted.get('vendor_name', '')}'")
|
360
|
+
print(f" Invoice #: '{extracted.get('invoice_number', '')}'")
|
361
|
+
print(f" Date: '{extracted.get('invoice_date', '')}'")
|
362
|
+
print(f" Amount: {extracted.get('amount', 0)}")
|
363
|
+
print(f" Tax: {extracted.get('tax_amount', 0)}")
|
364
|
+
|
365
|
+
# Print insertion results
|
366
|
+
if tool_name == "PostgresInsert":
|
367
|
+
print(f"💾 Insertion Results:")
|
368
|
+
print(f" Record ID: {data.get('record_id', 'N/A')}")
|
369
|
+
print(f" Table: {data.get('database_table', 'N/A')}")
|
370
|
+
print(f" Success: {data.get('success', 'N/A')}")
|
371
|
+
if 'inserted_data' in data:
|
372
|
+
inserted = data['inserted_data']
|
373
|
+
if isinstance(inserted, dict) and 'extracted_data' in inserted:
|
374
|
+
extracted = inserted['extracted_data']
|
375
|
+
print(f" Inserted Data:")
|
376
|
+
print(f" Vendor: '{extracted.get('vendor_name', '')}'")
|
377
|
+
print(f" Invoice #: '{extracted.get('invoice_number', '')}'")
|
378
|
+
print(f" Date: '{extracted.get('invoice_date', '')}'")
|
379
|
+
print(f" Amount: {extracted.get('amount', 0)}")
|
380
|
+
print(f" Tax: {extracted.get('tax_amount', 0)}")
|
381
|
+
|
382
|
+
if 'error' in tool_result:
|
383
|
+
print(f"❌ Error: {tool_result['error']}")
|
384
|
+
|
385
|
+
print("=" * 60)
|
386
|
+
|
249
387
|
# Check if this tool did real work or mock work
|
250
388
|
tool_data = tool_result.get("data", {})
|
251
389
|
if self._is_real_work(tool_name, tool_data):
|
@@ -265,9 +403,29 @@ class ExecutionEngine:
|
|
265
403
|
"work_quality": "real" if tools_with_real_work else "mock"
|
266
404
|
}
|
267
405
|
|
406
|
+
# Call custom processing function if provided
|
407
|
+
if agent.custom_processing and callable(agent.custom_processing):
|
408
|
+
print(f"\n🔧 {agent.role}: Applying custom processing...")
|
409
|
+
try:
|
410
|
+
custom_result = agent.custom_processing(agent, result_data, **context)
|
411
|
+
if custom_result:
|
412
|
+
result_data = custom_result
|
413
|
+
except Exception as e:
|
414
|
+
print(f"⚠️ {agent.role}: Custom processing failed: {e}")
|
415
|
+
logger.warning(f"Custom processing failed for {agent.role}: {e}")
|
416
|
+
|
417
|
+
# Handle agents without tools - they should still be able to pass data
|
418
|
+
if len(agent.tools) == 0:
|
419
|
+
# Agent has no tools, but should still be able to pass input data through
|
420
|
+
print(f"📝 {agent.role}: I have no tools, but I'll pass through my input data")
|
421
|
+
# Pass through the input data as output
|
422
|
+
result_data.update(agent_input)
|
423
|
+
|
268
424
|
# Agent reports completion
|
269
425
|
if tools_with_real_work:
|
270
426
|
print(f"🎉 {agent.role}: Perfect! I completed my work with real data processing")
|
427
|
+
elif len(agent.tools) == 0:
|
428
|
+
print(f"📝 {agent.role}: I passed through my input data (no tools needed)")
|
271
429
|
else:
|
272
430
|
print(f"📝 {agent.role}: I finished my work, but used simulated data (still learning!)")
|
273
431
|
|
@@ -287,95 +445,108 @@ class ExecutionEngine:
|
|
287
445
|
}
|
288
446
|
|
289
447
|
def _is_real_work(self, tool_name: str, tool_data: Dict[str, Any]) -> bool:
|
290
|
-
"""Determine if a tool
|
448
|
+
"""Determine if a tool performed real work vs mock/simulated work"""
|
449
|
+
|
450
|
+
# Handle nested data structure from server tools
|
451
|
+
if "data" in tool_data and isinstance(tool_data["data"], dict):
|
452
|
+
# Server tools return nested structure: {"success": true, "data": {"success": true, "data": {...}}}
|
453
|
+
if "data" in tool_data["data"]:
|
454
|
+
actual_data = tool_data["data"]["data"]
|
455
|
+
else:
|
456
|
+
actual_data = tool_data["data"]
|
457
|
+
else:
|
458
|
+
actual_data = tool_data
|
291
459
|
|
292
460
|
# Check for specific indicators of real work
|
293
461
|
if tool_name == "PDFProcessor":
|
294
|
-
# Real work if it has actual
|
462
|
+
# Real work if it has actual extracted data with proper MCP format structure
|
295
463
|
return (
|
296
|
-
"
|
297
|
-
"
|
298
|
-
|
299
|
-
"
|
300
|
-
|
301
|
-
"
|
464
|
+
"extracted_data" in actual_data and
|
465
|
+
"headerSection" in actual_data["extracted_data"] and
|
466
|
+
"billingDetails" in actual_data["extracted_data"] and
|
467
|
+
"chargesSummary" in actual_data["extracted_data"] and
|
468
|
+
actual_data["extracted_data"]["headerSection"].get("vendorName", "") != "" and
|
469
|
+
actual_data["extracted_data"]["billingDetails"].get("invoiceNumber", "") != "" and
|
470
|
+
actual_data["extracted_data"]["billingDetails"].get("invoiceDate", "") != "" and
|
471
|
+
actual_data["extracted_data"]["chargesSummary"].get("document_total", 0) > 0
|
302
472
|
)
|
303
473
|
|
304
474
|
elif tool_name == "InvoiceExtractionWorkflow":
|
305
475
|
# Real work if it has actual extracted data with specific vendor info
|
306
476
|
return (
|
307
|
-
"
|
308
|
-
"
|
309
|
-
|
310
|
-
"
|
311
|
-
"
|
477
|
+
"extracted_data" in actual_data and
|
478
|
+
"vendor_name" in actual_data["extracted_data"] and
|
479
|
+
"invoice_number" in actual_data["extracted_data"] and
|
480
|
+
"invoice_date" in actual_data["extracted_data"] and
|
481
|
+
actual_data["extracted_data"]["invoice_date"] != "" and # Valid date
|
482
|
+
actual_data["extracted_data"]["vendor_name"] not in ["", "UNKNOWN", "Sample Vendor"]
|
312
483
|
)
|
313
484
|
|
314
485
|
elif tool_name == "DatabaseQueryTool":
|
315
486
|
# Real work if it loaded the actual schema file (more than 3 columns)
|
316
487
|
return (
|
317
|
-
"columns" in
|
318
|
-
len(
|
488
|
+
"columns" in actual_data and
|
489
|
+
len(actual_data["columns"]) > 3
|
319
490
|
)
|
320
491
|
|
321
492
|
elif tool_name == "DataValidator":
|
322
493
|
# Real work if it actually validated real data with meaningful validation
|
323
494
|
return (
|
324
|
-
"validation_errors" in
|
325
|
-
isinstance(
|
326
|
-
"is_valid" in
|
495
|
+
"validation_errors" in actual_data and
|
496
|
+
isinstance(actual_data["validation_errors"], list) and
|
497
|
+
"is_valid" in actual_data and
|
327
498
|
# Check if it's validating real extracted data (not just mock data)
|
328
|
-
len(str(
|
329
|
-
not
|
499
|
+
len(str(actual_data)) > 100 and # Real validation results are more substantial
|
500
|
+
not actual_data.get("_mock", False) # Not mock data
|
330
501
|
)
|
331
502
|
|
332
503
|
elif tool_name == "PostgresInsert":
|
333
504
|
# Real work if it successfully inserted into a real database
|
334
505
|
return (
|
335
|
-
"success" in
|
336
|
-
|
337
|
-
"record_id" in
|
338
|
-
isinstance(
|
339
|
-
"database_table" in
|
340
|
-
not
|
506
|
+
"success" in actual_data and
|
507
|
+
actual_data["success"] == True and
|
508
|
+
"record_id" in actual_data and
|
509
|
+
isinstance(actual_data["record_id"], int) and # Real DB returns integer IDs
|
510
|
+
"database_table" in actual_data and # Real implementation includes table name
|
511
|
+
not actual_data.get("_mock", False) # Not mock data
|
341
512
|
)
|
342
513
|
|
343
514
|
elif tool_name == "FileDiscovery":
|
344
515
|
# Real work if it actually discovered files in a real directory
|
345
516
|
return (
|
346
|
-
"files" in
|
347
|
-
isinstance(
|
348
|
-
"directory" in
|
349
|
-
|
517
|
+
"files" in actual_data and
|
518
|
+
isinstance(actual_data["files"], list) and
|
519
|
+
"directory" in actual_data and
|
520
|
+
actual_data.get("success", False) == True
|
350
521
|
)
|
351
522
|
|
352
523
|
elif tool_name == "FileCopy":
|
353
524
|
# Real work if it actually copied a file
|
354
525
|
return (
|
355
|
-
"destination_path" in
|
356
|
-
"source_path" in
|
357
|
-
|
358
|
-
|
526
|
+
"destination_path" in actual_data and
|
527
|
+
"source_path" in actual_data and
|
528
|
+
actual_data.get("success", False) == True and
|
529
|
+
actual_data.get("operation") == "copy_completed"
|
359
530
|
)
|
360
531
|
|
361
532
|
elif tool_name == "TextToSQL":
|
362
533
|
# Real work if it actually executed SQL and returned real results
|
363
534
|
return (
|
364
|
-
"generated_sql" in
|
365
|
-
"results" in
|
366
|
-
isinstance(
|
367
|
-
|
368
|
-
not
|
535
|
+
"generated_sql" in actual_data and
|
536
|
+
"results" in actual_data and
|
537
|
+
isinstance(actual_data["results"], list) and
|
538
|
+
actual_data.get("success", False) == True and
|
539
|
+
not actual_data.get("_mock", False) # Not mock data
|
369
540
|
)
|
370
541
|
|
371
542
|
elif tool_name == "SQLExecutor":
|
372
543
|
# Real work if it actually executed SQL and returned real results
|
373
544
|
return (
|
374
|
-
"query" in
|
375
|
-
"results" in
|
376
|
-
isinstance(
|
377
|
-
"row_count" in
|
378
|
-
not
|
545
|
+
"query" in actual_data and
|
546
|
+
"results" in actual_data and
|
547
|
+
isinstance(actual_data["results"], list) and
|
548
|
+
"row_count" in actual_data and
|
549
|
+
not actual_data.get("_mock", False) # Not mock data
|
379
550
|
)
|
380
551
|
|
381
552
|
# Default to mock work
|
memra/models.py
CHANGED
@@ -26,6 +26,7 @@ class Agent(BaseModel):
|
|
26
26
|
allow_delegation: bool = False
|
27
27
|
fallback_agents: Optional[Dict[str, str]] = None
|
28
28
|
config: Optional[Dict[str, Any]] = None
|
29
|
+
custom_processing: Optional[Any] = None # Function to call after tool execution
|
29
30
|
|
30
31
|
class ExecutionPolicy(BaseModel):
|
31
32
|
retry_on_fail: bool = True
|
memra/tool_registry_client.py
CHANGED
@@ -62,7 +62,7 @@ class ToolRegistryClient:
|
|
62
62
|
}
|
63
63
|
|
64
64
|
# Make API call
|
65
|
-
with httpx.Client(timeout=
|
65
|
+
with httpx.Client(timeout=60.0) as client: # Reduced timeout for faster response
|
66
66
|
response = client.post(
|
67
67
|
f"{self.api_base}/tools/execute",
|
68
68
|
headers={
|
@@ -81,7 +81,7 @@ class ToolRegistryClient:
|
|
81
81
|
logger.error(f"Tool {tool_name} execution timed out")
|
82
82
|
return {
|
83
83
|
"success": False,
|
84
|
-
"error": f"Tool execution timed out after
|
84
|
+
"error": f"Tool execution timed out after 60 seconds"
|
85
85
|
}
|
86
86
|
except httpx.HTTPStatusError as e:
|
87
87
|
logger.error(f"API error for tool {tool_name}: {e.response.status_code}")
|