memra 0.0.0__tar.gz
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-0.0.0/LICENSE +0 -0
- memra-0.0.0/PKG-INFO +5 -0
- memra-0.0.0/README.md +164 -0
- memra-0.0.0/memra/__init__.py +28 -0
- memra-0.0.0/memra/discovery_client.py +49 -0
- memra-0.0.0/memra/execution.py +418 -0
- memra-0.0.0/memra/models.py +98 -0
- memra-0.0.0/memra/tool_registry_client.py +100 -0
- memra-0.0.0/memra.egg-info/PKG-INFO +5 -0
- memra-0.0.0/memra.egg-info/SOURCES.txt +12 -0
- memra-0.0.0/memra.egg-info/dependency_links.txt +1 -0
- memra-0.0.0/memra.egg-info/top_level.txt +1 -0
- memra-0.0.0/pyproject.toml +0 -0
- memra-0.0.0/setup.cfg +4 -0
memra-0.0.0/LICENSE
ADDED
File without changes
|
memra-0.0.0/PKG-INFO
ADDED
memra-0.0.0/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# Memra SDK
|
2
|
+
|
3
|
+
A declarative orchestration framework for AI-powered business workflows. Think of it as "Kubernetes for business logic" where agents are the pods and departments are the deployments.
|
4
|
+
|
5
|
+
## 🚀 Quick Start
|
6
|
+
|
7
|
+
```python
|
8
|
+
from memra import Agent, Department, LLM
|
9
|
+
from memra.execution import ExecutionEngine
|
10
|
+
|
11
|
+
# Define your agents declaratively
|
12
|
+
data_engineer = Agent(
|
13
|
+
role="Data Engineer",
|
14
|
+
job="Extract invoice schema from database",
|
15
|
+
llm=LLM(model="llama-3.2-11b-vision-preview", temperature=0.1),
|
16
|
+
tools=[
|
17
|
+
{"name": "DatabaseQueryTool", "hosted_by": "memra"}
|
18
|
+
],
|
19
|
+
output_key="invoice_schema"
|
20
|
+
)
|
21
|
+
|
22
|
+
invoice_parser = Agent(
|
23
|
+
role="Invoice Parser",
|
24
|
+
job="Extract structured data from invoice PDF",
|
25
|
+
llm=LLM(model="llama-3.2-11b-vision-preview", temperature=0.0),
|
26
|
+
tools=[
|
27
|
+
{"name": "PDFProcessor", "hosted_by": "memra"},
|
28
|
+
{"name": "InvoiceExtractionWorkflow", "hosted_by": "memra"}
|
29
|
+
],
|
30
|
+
input_keys=["file", "invoice_schema"],
|
31
|
+
output_key="invoice_data"
|
32
|
+
)
|
33
|
+
|
34
|
+
# Create a department
|
35
|
+
ap_department = Department(
|
36
|
+
name="Accounts Payable",
|
37
|
+
mission="Process invoices accurately into financial system",
|
38
|
+
agents=[data_engineer, invoice_parser],
|
39
|
+
workflow_order=["Data Engineer", "Invoice Parser"]
|
40
|
+
)
|
41
|
+
|
42
|
+
# Execute the workflow
|
43
|
+
engine = ExecutionEngine()
|
44
|
+
result = engine.execute_department(ap_department, {
|
45
|
+
"file": "invoice.pdf",
|
46
|
+
"connection": "postgresql://user@host/db"
|
47
|
+
})
|
48
|
+
|
49
|
+
if result.success:
|
50
|
+
print("✅ Invoice processed successfully!")
|
51
|
+
print(f"Data: {result.data}")
|
52
|
+
```
|
53
|
+
|
54
|
+
## 📦 Installation
|
55
|
+
|
56
|
+
```bash
|
57
|
+
pip install memra-sdk
|
58
|
+
```
|
59
|
+
|
60
|
+
## 🔑 Configuration
|
61
|
+
|
62
|
+
### Step 1: Get Your API Key
|
63
|
+
Contact **info@memra.co** to request an API key for early access.
|
64
|
+
|
65
|
+
### Step 2: Set Your API Key
|
66
|
+
Once you receive your API key, configure it:
|
67
|
+
|
68
|
+
```bash
|
69
|
+
export MEMRA_API_KEY="your-api-key-here"
|
70
|
+
export MEMRA_API_URL="https://api.memra.co" # Optional, defaults to production
|
71
|
+
```
|
72
|
+
|
73
|
+
Or in Python:
|
74
|
+
|
75
|
+
```python
|
76
|
+
import os
|
77
|
+
os.environ["MEMRA_API_KEY"] = "your-api-key-here"
|
78
|
+
```
|
79
|
+
|
80
|
+
## 🎯 Key Features
|
81
|
+
|
82
|
+
- **Declarative**: Define workflows like Kubernetes YAML
|
83
|
+
- **AI-Powered**: Built-in LLM integrations for document processing
|
84
|
+
- **Conversational**: Agents explain what they're doing in real-time
|
85
|
+
- **Production Ready**: Real database connectivity and file processing
|
86
|
+
- **Scalable**: Tools execute on Memra's cloud infrastructure
|
87
|
+
|
88
|
+
## 🏗️ Core Concepts
|
89
|
+
|
90
|
+
### Agents
|
91
|
+
Agents are the workers in your workflow. Each agent has:
|
92
|
+
- **Role**: What they do (e.g., "Data Engineer")
|
93
|
+
- **Job**: Their specific responsibility
|
94
|
+
- **Tools**: What capabilities they use
|
95
|
+
- **LLM**: Their AI model configuration
|
96
|
+
|
97
|
+
### Departments
|
98
|
+
Departments coordinate multiple agents:
|
99
|
+
- **Mission**: Overall goal
|
100
|
+
- **Workflow Order**: Sequence of agent execution
|
101
|
+
- **Manager**: Optional oversight and validation
|
102
|
+
|
103
|
+
### Tools
|
104
|
+
Tools are hosted capabilities:
|
105
|
+
- **memra**: Hosted by Memra (PDF processing, LLMs, databases)
|
106
|
+
- **mcp**: Customer-hosted via Model Context Protocol
|
107
|
+
|
108
|
+
## 📊 Example Output
|
109
|
+
|
110
|
+
```
|
111
|
+
🏢 Starting Accounts Payable Department
|
112
|
+
📋 Mission: Process invoices accurately into financial system
|
113
|
+
👥 Team: Data Engineer, Invoice Parser
|
114
|
+
🔄 Workflow: Data Engineer → Invoice Parser
|
115
|
+
|
116
|
+
👤 Data Engineer: Hi! I'm starting my work now...
|
117
|
+
💭 Data Engineer: My job is to extract invoice schema from database
|
118
|
+
⚡ Data Engineer: Using tool 1/1: DatabaseQueryTool
|
119
|
+
✅ Data Engineer: Great! DatabaseQueryTool did real work and gave me useful results
|
120
|
+
🎉 Data Engineer: Perfect! I completed my work with real data processing
|
121
|
+
|
122
|
+
👤 Invoice Parser: Hi! I'm starting my work now...
|
123
|
+
💭 Invoice Parser: My job is to extract structured data from invoice pdf
|
124
|
+
⚡ Invoice Parser: Using tool 1/2: PDFProcessor
|
125
|
+
✅ Invoice Parser: Great! PDFProcessor did real work and gave me useful results
|
126
|
+
```
|
127
|
+
|
128
|
+
## 🔍 Tool Discovery
|
129
|
+
|
130
|
+
Discover available tools:
|
131
|
+
|
132
|
+
```python
|
133
|
+
from memra import discover_tools, get_api_status
|
134
|
+
|
135
|
+
# Check API health
|
136
|
+
status = get_api_status()
|
137
|
+
print(f"API Health: {status['api_healthy']}")
|
138
|
+
print(f"Tools Available: {status['tools_available']}")
|
139
|
+
|
140
|
+
# Discover tools
|
141
|
+
tools = discover_tools()
|
142
|
+
for tool in tools:
|
143
|
+
print(f"- {tool['name']}: {tool['description']}")
|
144
|
+
```
|
145
|
+
|
146
|
+
## 📚 Examples
|
147
|
+
|
148
|
+
See the `examples/` directory for complete workflows:
|
149
|
+
|
150
|
+
- `accounts_payable_client.py`: Invoice processing with database integration
|
151
|
+
- More examples coming soon!
|
152
|
+
|
153
|
+
## 🆘 Support
|
154
|
+
|
155
|
+
- **Support**: info@memra.co
|
156
|
+
|
157
|
+
## 📄 License
|
158
|
+
|
159
|
+
MIT License - see LICENSE file for details.
|
160
|
+
|
161
|
+
---
|
162
|
+
|
163
|
+
**Built with ❤️ by the Memra team**
|
164
|
+
**Note**: The SDK will not work without a valid API key.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"""
|
2
|
+
Memra SDK - A declarative orchestration framework for AI-powered business workflows
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .models import (
|
6
|
+
Agent,
|
7
|
+
Department,
|
8
|
+
LLM,
|
9
|
+
Tool,
|
10
|
+
ExecutionPolicy,
|
11
|
+
ExecutionTrace,
|
12
|
+
DepartmentResult,
|
13
|
+
DepartmentAudit
|
14
|
+
)
|
15
|
+
from .discovery_client import discover_tools, check_api_health, get_api_status
|
16
|
+
|
17
|
+
__version__ = "0.1.0"
|
18
|
+
__all__ = [
|
19
|
+
"Agent",
|
20
|
+
"Department",
|
21
|
+
"LLM",
|
22
|
+
"Tool",
|
23
|
+
"ExecutionPolicy",
|
24
|
+
"ExecutionTrace",
|
25
|
+
"DepartmentResult",
|
26
|
+
"DepartmentAudit",
|
27
|
+
"discover_tools"
|
28
|
+
]
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Client-side tool discovery for Memra SDK
|
3
|
+
Queries the Memra API to discover available tools
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import List, Dict, Any, Optional
|
7
|
+
from .tool_registry_client import ToolRegistryClient
|
8
|
+
|
9
|
+
def discover_tools(hosted_by: Optional[str] = None) -> List[Dict[str, Any]]:
|
10
|
+
"""
|
11
|
+
Discover available tools from the Memra API
|
12
|
+
|
13
|
+
Args:
|
14
|
+
hosted_by: Filter tools by hosting provider ("memra" or "mcp")
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
List of available tools with their descriptions
|
18
|
+
"""
|
19
|
+
registry = ToolRegistryClient()
|
20
|
+
return registry.discover_tools(hosted_by)
|
21
|
+
|
22
|
+
def check_api_health() -> bool:
|
23
|
+
"""
|
24
|
+
Check if the Memra API is available
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
True if API is healthy, False otherwise
|
28
|
+
"""
|
29
|
+
registry = ToolRegistryClient()
|
30
|
+
return registry.health_check()
|
31
|
+
|
32
|
+
def get_api_status() -> Dict[str, Any]:
|
33
|
+
"""
|
34
|
+
Get detailed API status information
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Dictionary with API status details
|
38
|
+
"""
|
39
|
+
registry = ToolRegistryClient()
|
40
|
+
|
41
|
+
is_healthy = registry.health_check()
|
42
|
+
tools = registry.discover_tools() if is_healthy else []
|
43
|
+
|
44
|
+
return {
|
45
|
+
"api_healthy": is_healthy,
|
46
|
+
"api_url": registry.api_base,
|
47
|
+
"tools_available": len(tools),
|
48
|
+
"tools": tools
|
49
|
+
}
|
@@ -0,0 +1,418 @@
|
|
1
|
+
import time
|
2
|
+
import logging
|
3
|
+
from typing import Dict, Any, List, Optional
|
4
|
+
from .models import Department, Agent, DepartmentResult, ExecutionTrace, DepartmentAudit
|
5
|
+
from .tool_registry_client import ToolRegistryClient
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
class ExecutionEngine:
|
10
|
+
"""Engine that executes department workflows by coordinating agents and tools"""
|
11
|
+
|
12
|
+
def __init__(self):
|
13
|
+
self.tool_registry = ToolRegistryClient()
|
14
|
+
self.last_execution_audit: Optional[DepartmentAudit] = None
|
15
|
+
|
16
|
+
def execute_department(self, department: Department, input_data: Dict[str, Any]) -> DepartmentResult:
|
17
|
+
"""Execute a department workflow"""
|
18
|
+
start_time = time.time()
|
19
|
+
trace = ExecutionTrace()
|
20
|
+
|
21
|
+
try:
|
22
|
+
print(f"\n🏢 Starting {department.name} Department")
|
23
|
+
print(f"📋 Mission: {department.mission}")
|
24
|
+
print(f"👥 Team: {', '.join([agent.role for agent in department.agents])}")
|
25
|
+
if department.manager_agent:
|
26
|
+
print(f"👔 Manager: {department.manager_agent.role}")
|
27
|
+
print(f"🔄 Workflow: {' → '.join(department.workflow_order)}")
|
28
|
+
print("=" * 60)
|
29
|
+
|
30
|
+
logger.info(f"Starting execution of department: {department.name}")
|
31
|
+
|
32
|
+
# Initialize execution context
|
33
|
+
context = {
|
34
|
+
"input": input_data,
|
35
|
+
"department_context": department.context or {},
|
36
|
+
"results": {}
|
37
|
+
}
|
38
|
+
|
39
|
+
# Execute agents in workflow order
|
40
|
+
for i, agent_role in enumerate(department.workflow_order, 1):
|
41
|
+
print(f"\n🔄 Step {i}/{len(department.workflow_order)}: {agent_role}")
|
42
|
+
|
43
|
+
agent = self._find_agent_by_role(department, agent_role)
|
44
|
+
if not agent:
|
45
|
+
error_msg = f"Agent with role '{agent_role}' not found in department"
|
46
|
+
print(f"❌ Error: {error_msg}")
|
47
|
+
trace.errors.append(error_msg)
|
48
|
+
return DepartmentResult(
|
49
|
+
success=False,
|
50
|
+
error=error_msg,
|
51
|
+
trace=trace
|
52
|
+
)
|
53
|
+
|
54
|
+
# Execute agent
|
55
|
+
agent_start = time.time()
|
56
|
+
result = self._execute_agent(agent, context, trace)
|
57
|
+
agent_duration = time.time() - agent_start
|
58
|
+
|
59
|
+
trace.agents_executed.append(agent.role)
|
60
|
+
trace.execution_times[agent.role] = agent_duration
|
61
|
+
|
62
|
+
if not result.get("success", False):
|
63
|
+
# Try fallback if available
|
64
|
+
if department.manager_agent and agent.role in (department.manager_agent.fallback_agents or {}):
|
65
|
+
fallback_role = department.manager_agent.fallback_agents[agent.role]
|
66
|
+
print(f"🔄 {department.manager_agent.role}: Let me try {fallback_role} as backup for {agent.role}")
|
67
|
+
fallback_agent = self._find_agent_by_role(department, fallback_role)
|
68
|
+
if fallback_agent:
|
69
|
+
logger.info(f"Trying fallback agent: {fallback_role}")
|
70
|
+
result = self._execute_agent(fallback_agent, context, trace)
|
71
|
+
trace.agents_executed.append(fallback_agent.role)
|
72
|
+
|
73
|
+
if not result.get("success", False):
|
74
|
+
error_msg = f"Agent {agent.role} failed: {result.get('error', 'Unknown error')}"
|
75
|
+
print(f"❌ Workflow stopped: {error_msg}")
|
76
|
+
trace.errors.append(error_msg)
|
77
|
+
return DepartmentResult(
|
78
|
+
success=False,
|
79
|
+
error=error_msg,
|
80
|
+
trace=trace
|
81
|
+
)
|
82
|
+
|
83
|
+
# Store result for next agent
|
84
|
+
context["results"][agent.output_key] = result.get("data")
|
85
|
+
print(f"✅ Step {i} completed in {agent_duration:.1f}s")
|
86
|
+
|
87
|
+
# Execute manager agent for final validation if present
|
88
|
+
if department.manager_agent:
|
89
|
+
print(f"\n🔍 Final Review Phase")
|
90
|
+
manager_start = time.time()
|
91
|
+
|
92
|
+
# Prepare manager input with all workflow results
|
93
|
+
manager_input = {
|
94
|
+
"workflow_results": context["results"],
|
95
|
+
"department_context": context["department_context"]
|
96
|
+
}
|
97
|
+
|
98
|
+
# Add connection if available
|
99
|
+
if "connection" in context["input"]:
|
100
|
+
manager_input["connection"] = context["input"]["connection"]
|
101
|
+
|
102
|
+
# Execute manager validation
|
103
|
+
manager_result = self._execute_manager_validation(department.manager_agent, manager_input, trace)
|
104
|
+
manager_duration = time.time() - manager_start
|
105
|
+
|
106
|
+
trace.agents_executed.append(department.manager_agent.role)
|
107
|
+
trace.execution_times[department.manager_agent.role] = manager_duration
|
108
|
+
|
109
|
+
# Store manager validation results
|
110
|
+
context["results"][department.manager_agent.output_key] = manager_result.get("data")
|
111
|
+
|
112
|
+
# Check if manager validation failed
|
113
|
+
if not manager_result.get("success", False):
|
114
|
+
error_msg = f"Manager validation failed: {manager_result.get('error', 'Unknown error')}"
|
115
|
+
print(f"❌ {error_msg}")
|
116
|
+
trace.errors.append(error_msg)
|
117
|
+
return DepartmentResult(
|
118
|
+
success=False,
|
119
|
+
error=error_msg,
|
120
|
+
trace=trace
|
121
|
+
)
|
122
|
+
|
123
|
+
print(f"✅ Manager review completed in {manager_duration:.1f}s")
|
124
|
+
|
125
|
+
# Create audit record
|
126
|
+
total_duration = time.time() - start_time
|
127
|
+
self.last_execution_audit = DepartmentAudit(
|
128
|
+
agents_run=trace.agents_executed,
|
129
|
+
tools_invoked=trace.tools_invoked,
|
130
|
+
duration_seconds=total_duration
|
131
|
+
)
|
132
|
+
|
133
|
+
print(f"\n🎉 {department.name} Department workflow completed!")
|
134
|
+
print(f"⏱️ Total time: {total_duration:.1f}s")
|
135
|
+
print("=" * 60)
|
136
|
+
|
137
|
+
return DepartmentResult(
|
138
|
+
success=True,
|
139
|
+
data=context["results"],
|
140
|
+
trace=trace
|
141
|
+
)
|
142
|
+
|
143
|
+
except Exception as e:
|
144
|
+
print(f"💥 Unexpected error in {department.name} Department: {str(e)}")
|
145
|
+
logger.error(f"Execution failed: {str(e)}")
|
146
|
+
trace.errors.append(str(e))
|
147
|
+
return DepartmentResult(
|
148
|
+
success=False,
|
149
|
+
error=str(e),
|
150
|
+
trace=trace
|
151
|
+
)
|
152
|
+
|
153
|
+
def _find_agent_by_role(self, department: Department, role: str) -> Optional[Agent]:
|
154
|
+
"""Find an agent by role in the department"""
|
155
|
+
for agent in department.agents:
|
156
|
+
if agent.role == role:
|
157
|
+
return agent
|
158
|
+
return None
|
159
|
+
|
160
|
+
def _execute_agent(self, agent: Agent, context: Dict[str, Any], trace: ExecutionTrace) -> Dict[str, Any]:
|
161
|
+
"""Execute a single agent"""
|
162
|
+
print(f"\n👤 {agent.role}: Hi! I'm starting my work now...")
|
163
|
+
logger.info(f"Executing agent: {agent.role}")
|
164
|
+
|
165
|
+
try:
|
166
|
+
# Show what the agent is thinking about
|
167
|
+
print(f"💭 {agent.role}: My job is to {agent.job.lower()}")
|
168
|
+
|
169
|
+
# Prepare input data for agent
|
170
|
+
agent_input = {}
|
171
|
+
for key in agent.input_keys:
|
172
|
+
if key in context["input"]:
|
173
|
+
agent_input[key] = context["input"][key]
|
174
|
+
print(f"📥 {agent.role}: I received '{key}' as input")
|
175
|
+
elif key in context["results"]:
|
176
|
+
agent_input[key] = context["results"][key]
|
177
|
+
print(f"📥 {agent.role}: I got '{key}' from a previous agent")
|
178
|
+
else:
|
179
|
+
print(f"🤔 {agent.role}: Hmm, I'm missing input '{key}' but I'll try to work without it")
|
180
|
+
logger.warning(f"Missing input key '{key}' for agent {agent.role}")
|
181
|
+
|
182
|
+
# Always include connection string if available (for database tools)
|
183
|
+
if "connection" in context["input"]:
|
184
|
+
agent_input["connection"] = context["input"]["connection"]
|
185
|
+
|
186
|
+
# Execute agent's tools
|
187
|
+
result_data = {}
|
188
|
+
tools_with_real_work = []
|
189
|
+
tools_with_mock_work = []
|
190
|
+
|
191
|
+
print(f"🔧 {agent.role}: I need to use {len(agent.tools)} tool(s) to complete my work...")
|
192
|
+
|
193
|
+
for i, tool_spec in enumerate(agent.tools, 1):
|
194
|
+
tool_name = tool_spec["name"] if isinstance(tool_spec, dict) else tool_spec.name
|
195
|
+
hosted_by = tool_spec.get("hosted_by", "memra") if isinstance(tool_spec, dict) else tool_spec.hosted_by
|
196
|
+
|
197
|
+
print(f"⚡ {agent.role}: Using tool {i}/{len(agent.tools)}: {tool_name}")
|
198
|
+
|
199
|
+
trace.tools_invoked.append(tool_name)
|
200
|
+
|
201
|
+
# Get tool from registry and execute
|
202
|
+
tool_result = self.tool_registry.execute_tool(
|
203
|
+
tool_name,
|
204
|
+
hosted_by,
|
205
|
+
agent_input,
|
206
|
+
agent.config
|
207
|
+
)
|
208
|
+
|
209
|
+
if not tool_result.get("success", False):
|
210
|
+
print(f"😟 {agent.role}: Oh no! Tool {tool_name} failed: {tool_result.get('error', 'Unknown error')}")
|
211
|
+
return {
|
212
|
+
"success": False,
|
213
|
+
"error": f"Tool {tool_name} failed: {tool_result.get('error', 'Unknown error')}"
|
214
|
+
}
|
215
|
+
|
216
|
+
# Check if this tool did real work or mock work
|
217
|
+
tool_data = tool_result.get("data", {})
|
218
|
+
if self._is_real_work(tool_name, tool_data):
|
219
|
+
tools_with_real_work.append(tool_name)
|
220
|
+
print(f"✅ {agent.role}: Great! {tool_name} did real work and gave me useful results")
|
221
|
+
else:
|
222
|
+
tools_with_mock_work.append(tool_name)
|
223
|
+
print(f"🔄 {agent.role}: {tool_name} gave me simulated results (that's okay for testing)")
|
224
|
+
|
225
|
+
result_data.update(tool_data)
|
226
|
+
|
227
|
+
# Add metadata about real vs mock work
|
228
|
+
result_data["_memra_metadata"] = {
|
229
|
+
"agent_role": agent.role,
|
230
|
+
"tools_real_work": tools_with_real_work,
|
231
|
+
"tools_mock_work": tools_with_mock_work,
|
232
|
+
"work_quality": "real" if tools_with_real_work else "mock"
|
233
|
+
}
|
234
|
+
|
235
|
+
# Agent reports completion
|
236
|
+
if tools_with_real_work:
|
237
|
+
print(f"🎉 {agent.role}: Perfect! I completed my work with real data processing")
|
238
|
+
else:
|
239
|
+
print(f"📝 {agent.role}: I finished my work, but used simulated data (still learning!)")
|
240
|
+
|
241
|
+
print(f"📤 {agent.role}: Passing my results to the next agent via '{agent.output_key}'")
|
242
|
+
|
243
|
+
return {
|
244
|
+
"success": True,
|
245
|
+
"data": result_data
|
246
|
+
}
|
247
|
+
|
248
|
+
except Exception as e:
|
249
|
+
print(f"😰 {agent.role}: I encountered an error and couldn't complete my work: {str(e)}")
|
250
|
+
logger.error(f"Agent {agent.role} execution failed: {str(e)}")
|
251
|
+
return {
|
252
|
+
"success": False,
|
253
|
+
"error": str(e)
|
254
|
+
}
|
255
|
+
|
256
|
+
def _is_real_work(self, tool_name: str, tool_data: Dict[str, Any]) -> bool:
|
257
|
+
"""Determine if a tool did real work or returned mock data"""
|
258
|
+
|
259
|
+
# Check for specific indicators of real work
|
260
|
+
if tool_name == "PDFProcessor":
|
261
|
+
# Real work if it has actual image paths and file size
|
262
|
+
return (
|
263
|
+
"metadata" in tool_data and
|
264
|
+
"file_size" in tool_data["metadata"] and
|
265
|
+
tool_data["metadata"]["file_size"] > 1000 and # Real file size
|
266
|
+
"pages" in tool_data and
|
267
|
+
len(tool_data["pages"]) > 0 and
|
268
|
+
"image_path" in tool_data["pages"][0]
|
269
|
+
)
|
270
|
+
|
271
|
+
elif tool_name == "InvoiceExtractionWorkflow":
|
272
|
+
# Real work if it has actual extracted data with specific vendor info
|
273
|
+
return (
|
274
|
+
"headerSection" in tool_data and
|
275
|
+
"vendorName" in tool_data["headerSection"] and
|
276
|
+
tool_data["headerSection"]["vendorName"] not in ["", "UNKNOWN", "Sample Vendor"] and
|
277
|
+
"chargesSummary" in tool_data and
|
278
|
+
"memra_checksum" in tool_data["chargesSummary"]
|
279
|
+
)
|
280
|
+
|
281
|
+
elif tool_name == "DatabaseQueryTool":
|
282
|
+
# Real work if it loaded the actual schema file (more than 3 columns)
|
283
|
+
return (
|
284
|
+
"columns" in tool_data and
|
285
|
+
len(tool_data["columns"]) > 3
|
286
|
+
)
|
287
|
+
|
288
|
+
elif tool_name == "DataValidator":
|
289
|
+
# Real work if it actually validated real data with meaningful validation
|
290
|
+
return (
|
291
|
+
"validation_errors" in tool_data and
|
292
|
+
isinstance(tool_data["validation_errors"], list) and
|
293
|
+
"is_valid" in tool_data and
|
294
|
+
# Check if it's validating real extracted data (not just mock data)
|
295
|
+
len(str(tool_data)) > 100 # Real validation results are more substantial
|
296
|
+
)
|
297
|
+
|
298
|
+
elif tool_name == "PostgresInsert":
|
299
|
+
# Real work if it successfully inserted into a real database
|
300
|
+
return (
|
301
|
+
"success" in tool_data and
|
302
|
+
tool_data["success"] == True and
|
303
|
+
"record_id" in tool_data and
|
304
|
+
isinstance(tool_data["record_id"], int) and # Real DB returns integer IDs
|
305
|
+
"database_table" in tool_data # Real implementation includes table name
|
306
|
+
)
|
307
|
+
|
308
|
+
# Default to mock work
|
309
|
+
return False
|
310
|
+
|
311
|
+
def get_last_audit(self) -> Optional[DepartmentAudit]:
|
312
|
+
"""Get audit information from the last execution"""
|
313
|
+
return self.last_execution_audit
|
314
|
+
|
315
|
+
def _execute_manager_validation(self, manager_agent: Agent, manager_input: Dict[str, Any], trace: ExecutionTrace) -> Dict[str, Any]:
|
316
|
+
"""Execute manager agent to validate workflow results"""
|
317
|
+
print(f"\n👔 {manager_agent.role}: Time for me to review everyone's work...")
|
318
|
+
logger.info(f"Manager {manager_agent.role} validating workflow results")
|
319
|
+
|
320
|
+
try:
|
321
|
+
# Analyze workflow results for real vs mock work
|
322
|
+
workflow_analysis = self._analyze_workflow_quality(manager_input["workflow_results"])
|
323
|
+
|
324
|
+
print(f"🔍 {manager_agent.role}: Let me analyze what each agent accomplished...")
|
325
|
+
|
326
|
+
# Prepare validation report
|
327
|
+
validation_report = {
|
328
|
+
"workflow_analysis": workflow_analysis,
|
329
|
+
"validation_status": "pass" if workflow_analysis["overall_quality"] == "real" else "fail",
|
330
|
+
"recommendations": [],
|
331
|
+
"agent_performance": {}
|
332
|
+
}
|
333
|
+
|
334
|
+
# Analyze each agent's performance
|
335
|
+
for result_key, result_data in manager_input["workflow_results"].items():
|
336
|
+
if isinstance(result_data, dict) and "_memra_metadata" in result_data:
|
337
|
+
metadata = result_data["_memra_metadata"]
|
338
|
+
agent_role = metadata["agent_role"]
|
339
|
+
|
340
|
+
if metadata["work_quality"] == "real":
|
341
|
+
print(f"👍 {manager_agent.role}: {agent_role} did excellent real work!")
|
342
|
+
else:
|
343
|
+
print(f"📋 {manager_agent.role}: {agent_role} completed their tasks but with simulated data")
|
344
|
+
|
345
|
+
validation_report["agent_performance"][agent_role] = {
|
346
|
+
"work_quality": metadata["work_quality"],
|
347
|
+
"tools_real_work": metadata["tools_real_work"],
|
348
|
+
"tools_mock_work": metadata["tools_mock_work"],
|
349
|
+
"status": "completed_real_work" if metadata["work_quality"] == "real" else "completed_mock_work"
|
350
|
+
}
|
351
|
+
|
352
|
+
# Add recommendations for mock work
|
353
|
+
if metadata["work_quality"] == "mock":
|
354
|
+
recommendation = f"Agent {agent_role} performed mock work - implement real {', '.join(metadata['tools_mock_work'])} functionality"
|
355
|
+
validation_report["recommendations"].append(recommendation)
|
356
|
+
print(f"💡 {manager_agent.role}: I recommend upgrading {agent_role}'s tools for production")
|
357
|
+
|
358
|
+
# Overall workflow validation
|
359
|
+
if workflow_analysis["overall_quality"] == "real":
|
360
|
+
validation_report["summary"] = "Workflow completed successfully with real data processing"
|
361
|
+
print(f"🎯 {manager_agent.role}: Excellent! This workflow is production-ready")
|
362
|
+
elif workflow_analysis["overall_quality"].startswith("mixed"):
|
363
|
+
validation_report["summary"] = "Workflow completed with mixed real and simulated data"
|
364
|
+
print(f"⚖️ {manager_agent.role}: Good progress! Some agents are production-ready, others need work")
|
365
|
+
else:
|
366
|
+
validation_report["summary"] = "Workflow completed but with mock/simulated data - production readiness requires real implementations"
|
367
|
+
print(f"🚧 {manager_agent.role}: This workflow needs more development before production use")
|
368
|
+
|
369
|
+
real_percentage = workflow_analysis["real_work_percentage"]
|
370
|
+
print(f"📊 {manager_agent.role}: Overall assessment: {real_percentage:.0f}% of agents did real work")
|
371
|
+
|
372
|
+
return {
|
373
|
+
"success": True,
|
374
|
+
"data": validation_report
|
375
|
+
}
|
376
|
+
|
377
|
+
except Exception as e:
|
378
|
+
print(f"😰 {manager_agent.role}: I had trouble analyzing the workflow: {str(e)}")
|
379
|
+
logger.error(f"Manager validation failed: {str(e)}")
|
380
|
+
return {
|
381
|
+
"success": False,
|
382
|
+
"error": str(e)
|
383
|
+
}
|
384
|
+
|
385
|
+
def _analyze_workflow_quality(self, workflow_results: Dict[str, Any]) -> Dict[str, Any]:
|
386
|
+
"""Analyze the overall quality of workflow execution"""
|
387
|
+
|
388
|
+
total_agents = 0
|
389
|
+
real_work_agents = 0
|
390
|
+
mock_work_agents = 0
|
391
|
+
|
392
|
+
for result_key, result_data in workflow_results.items():
|
393
|
+
if isinstance(result_data, dict) and "_memra_metadata" in result_data:
|
394
|
+
metadata = result_data["_memra_metadata"]
|
395
|
+
total_agents += 1
|
396
|
+
|
397
|
+
if metadata["work_quality"] == "real":
|
398
|
+
real_work_agents += 1
|
399
|
+
else:
|
400
|
+
mock_work_agents += 1
|
401
|
+
|
402
|
+
# Determine overall quality
|
403
|
+
if real_work_agents > 0 and mock_work_agents == 0:
|
404
|
+
overall_quality = "real"
|
405
|
+
elif real_work_agents > mock_work_agents:
|
406
|
+
overall_quality = "mixed_mostly_real"
|
407
|
+
elif real_work_agents > 0:
|
408
|
+
overall_quality = "mixed_mostly_mock"
|
409
|
+
else:
|
410
|
+
overall_quality = "mock"
|
411
|
+
|
412
|
+
return {
|
413
|
+
"total_agents": total_agents,
|
414
|
+
"real_work_agents": real_work_agents,
|
415
|
+
"mock_work_agents": mock_work_agents,
|
416
|
+
"overall_quality": overall_quality,
|
417
|
+
"real_work_percentage": (real_work_agents / total_agents * 100) if total_agents > 0 else 0
|
418
|
+
}
|
@@ -0,0 +1,98 @@
|
|
1
|
+
from typing import List, Dict, Optional, Any, Union
|
2
|
+
from pydantic import BaseModel, Field
|
3
|
+
|
4
|
+
class LLM(BaseModel):
|
5
|
+
model: str
|
6
|
+
temperature: float = 0.0
|
7
|
+
max_tokens: Optional[int] = None
|
8
|
+
stop: Optional[List[str]] = None
|
9
|
+
|
10
|
+
class Tool(BaseModel):
|
11
|
+
name: str
|
12
|
+
hosted_by: str = "memra" # or "mcp" for customer's Model Context Protocol
|
13
|
+
description: Optional[str] = None
|
14
|
+
parameters: Optional[Dict[str, Any]] = None
|
15
|
+
|
16
|
+
class Agent(BaseModel):
|
17
|
+
role: str
|
18
|
+
job: str
|
19
|
+
llm: Optional[Union[LLM, Dict[str, Any]]] = None
|
20
|
+
sops: List[str] = Field(default_factory=list)
|
21
|
+
tools: List[Union[Tool, Dict[str, Any]]] = Field(default_factory=list)
|
22
|
+
systems: List[str] = Field(default_factory=list)
|
23
|
+
input_keys: List[str] = Field(default_factory=list)
|
24
|
+
output_key: str
|
25
|
+
allow_delegation: bool = False
|
26
|
+
fallback_agents: Optional[Dict[str, str]] = None
|
27
|
+
config: Optional[Dict[str, Any]] = None
|
28
|
+
|
29
|
+
class ExecutionPolicy(BaseModel):
|
30
|
+
retry_on_fail: bool = True
|
31
|
+
max_retries: int = 2
|
32
|
+
halt_on_validation_error: bool = True
|
33
|
+
timeout_seconds: int = 300
|
34
|
+
|
35
|
+
class ExecutionTrace(BaseModel):
|
36
|
+
agents_executed: List[str] = Field(default_factory=list)
|
37
|
+
tools_invoked: List[str] = Field(default_factory=list)
|
38
|
+
execution_times: Dict[str, float] = Field(default_factory=dict)
|
39
|
+
errors: List[str] = Field(default_factory=list)
|
40
|
+
|
41
|
+
def show(self):
|
42
|
+
"""Display execution trace information"""
|
43
|
+
print("=== Execution Trace ===")
|
44
|
+
print(f"Agents executed: {', '.join(self.agents_executed)}")
|
45
|
+
print(f"Tools invoked: {', '.join(self.tools_invoked)}")
|
46
|
+
if self.errors:
|
47
|
+
print(f"Errors: {', '.join(self.errors)}")
|
48
|
+
|
49
|
+
class DepartmentResult(BaseModel):
|
50
|
+
success: bool
|
51
|
+
data: Optional[Dict[str, Any]] = None
|
52
|
+
error: Optional[str] = None
|
53
|
+
trace: ExecutionTrace = Field(default_factory=ExecutionTrace)
|
54
|
+
|
55
|
+
class DepartmentAudit(BaseModel):
|
56
|
+
agents_run: List[str]
|
57
|
+
tools_invoked: List[str]
|
58
|
+
duration_seconds: float
|
59
|
+
total_cost: Optional[float] = None
|
60
|
+
|
61
|
+
class Department(BaseModel):
|
62
|
+
name: str
|
63
|
+
mission: str
|
64
|
+
agents: List[Agent]
|
65
|
+
manager_agent: Optional[Agent] = None
|
66
|
+
default_llm: Optional[LLM] = None
|
67
|
+
workflow_order: List[str] = Field(default_factory=list)
|
68
|
+
dependencies: List[str] = Field(default_factory=list)
|
69
|
+
execution_policy: Optional[ExecutionPolicy] = None
|
70
|
+
context: Optional[Dict[str, Any]] = None
|
71
|
+
|
72
|
+
def run(self, input: Dict[str, Any]) -> DepartmentResult:
|
73
|
+
"""
|
74
|
+
Execute the department workflow with the given input data.
|
75
|
+
"""
|
76
|
+
# Import here to avoid circular imports
|
77
|
+
from .execution import ExecutionEngine
|
78
|
+
|
79
|
+
engine = ExecutionEngine()
|
80
|
+
return engine.execute_department(self, input)
|
81
|
+
|
82
|
+
def audit(self) -> DepartmentAudit:
|
83
|
+
"""
|
84
|
+
Return audit information about the last execution.
|
85
|
+
"""
|
86
|
+
# Import here to avoid circular imports
|
87
|
+
from .execution import ExecutionEngine
|
88
|
+
|
89
|
+
engine = ExecutionEngine()
|
90
|
+
audit = engine.get_last_audit()
|
91
|
+
if audit:
|
92
|
+
return audit
|
93
|
+
else:
|
94
|
+
return DepartmentAudit(
|
95
|
+
agents_run=[],
|
96
|
+
tools_invoked=[],
|
97
|
+
duration_seconds=0.0
|
98
|
+
)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import httpx
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
from typing import Dict, Any, List, Optional
|
5
|
+
import asyncio
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
class ToolRegistryClient:
|
10
|
+
"""Client-side registry that calls Memra API for tool execution"""
|
11
|
+
|
12
|
+
def __init__(self):
|
13
|
+
self.api_base = os.getenv("MEMRA_API_URL", "https://memra-api.fly.dev")
|
14
|
+
self.api_key = os.getenv("MEMRA_API_KEY", "dev-key") # Default for development
|
15
|
+
self.tools_cache = None
|
16
|
+
|
17
|
+
def discover_tools(self, hosted_by: Optional[str] = None) -> List[Dict[str, Any]]:
|
18
|
+
"""Discover available tools from the API"""
|
19
|
+
try:
|
20
|
+
# Use sync httpx for compatibility with existing sync code
|
21
|
+
with httpx.Client(timeout=30.0) as client:
|
22
|
+
response = client.get(
|
23
|
+
f"{self.api_base}/tools/discover",
|
24
|
+
headers={"X-API-Key": self.api_key}
|
25
|
+
)
|
26
|
+
response.raise_for_status()
|
27
|
+
|
28
|
+
data = response.json()
|
29
|
+
tools = data.get("tools", [])
|
30
|
+
|
31
|
+
# Filter by hosted_by if specified
|
32
|
+
if hosted_by:
|
33
|
+
tools = [t for t in tools if t.get("hosted_by") == hosted_by]
|
34
|
+
|
35
|
+
self.tools_cache = tools
|
36
|
+
logger.info(f"Discovered {len(tools)} tools from API")
|
37
|
+
return tools
|
38
|
+
|
39
|
+
except Exception as e:
|
40
|
+
logger.error(f"Failed to discover tools from API: {e}")
|
41
|
+
# Return empty list if API is unavailable
|
42
|
+
return []
|
43
|
+
|
44
|
+
def execute_tool(self, tool_name: str, hosted_by: str, input_data: Dict[str, Any],
|
45
|
+
config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
46
|
+
"""Execute a tool via the API"""
|
47
|
+
try:
|
48
|
+
logger.info(f"Executing tool {tool_name} via API")
|
49
|
+
|
50
|
+
# Prepare request payload
|
51
|
+
payload = {
|
52
|
+
"tool_name": tool_name,
|
53
|
+
"hosted_by": hosted_by,
|
54
|
+
"input_data": input_data,
|
55
|
+
"config": config
|
56
|
+
}
|
57
|
+
|
58
|
+
# Make API call
|
59
|
+
with httpx.Client(timeout=120.0) as client: # Longer timeout for tool execution
|
60
|
+
response = client.post(
|
61
|
+
f"{self.api_base}/tools/execute",
|
62
|
+
headers={
|
63
|
+
"X-API-Key": self.api_key,
|
64
|
+
"Content-Type": "application/json"
|
65
|
+
},
|
66
|
+
json=payload
|
67
|
+
)
|
68
|
+
response.raise_for_status()
|
69
|
+
|
70
|
+
result = response.json()
|
71
|
+
logger.info(f"Tool {tool_name} executed successfully via API")
|
72
|
+
return result
|
73
|
+
|
74
|
+
except httpx.TimeoutException:
|
75
|
+
logger.error(f"Tool {tool_name} execution timed out")
|
76
|
+
return {
|
77
|
+
"success": False,
|
78
|
+
"error": f"Tool execution timed out after 120 seconds"
|
79
|
+
}
|
80
|
+
except httpx.HTTPStatusError as e:
|
81
|
+
logger.error(f"API error for tool {tool_name}: {e.response.status_code}")
|
82
|
+
return {
|
83
|
+
"success": False,
|
84
|
+
"error": f"API error: {e.response.status_code} - {e.response.text}"
|
85
|
+
}
|
86
|
+
except Exception as e:
|
87
|
+
logger.error(f"Tool execution failed for {tool_name}: {str(e)}")
|
88
|
+
return {
|
89
|
+
"success": False,
|
90
|
+
"error": str(e)
|
91
|
+
}
|
92
|
+
|
93
|
+
def health_check(self) -> bool:
|
94
|
+
"""Check if the API is available"""
|
95
|
+
try:
|
96
|
+
with httpx.Client(timeout=10.0) as client:
|
97
|
+
response = client.get(f"{self.api_base}/health")
|
98
|
+
return response.status_code == 200
|
99
|
+
except:
|
100
|
+
return False
|
@@ -0,0 +1,12 @@
|
|
1
|
+
LICENSE
|
2
|
+
README.md
|
3
|
+
pyproject.toml
|
4
|
+
memra/__init__.py
|
5
|
+
memra/discovery_client.py
|
6
|
+
memra/execution.py
|
7
|
+
memra/models.py
|
8
|
+
memra/tool_registry_client.py
|
9
|
+
memra.egg-info/PKG-INFO
|
10
|
+
memra.egg-info/SOURCES.txt
|
11
|
+
memra.egg-info/dependency_links.txt
|
12
|
+
memra.egg-info/top_level.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
memra
|
File without changes
|
memra-0.0.0/setup.cfg
ADDED