memra 0.0.1__py3-none-any.whl → 0.2.0__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/tool_registry.py ADDED
@@ -0,0 +1,190 @@
1
+ import importlib
2
+ import logging
3
+ import sys
4
+ import os
5
+ from typing import Dict, Any, List, Optional, Callable
6
+ from pathlib import Path
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ToolRegistry:
11
+ """Registry for managing and executing tools"""
12
+
13
+ def __init__(self):
14
+ self.tools: Dict[str, Dict[str, Any]] = {}
15
+ self._add_project_to_path()
16
+ self._load_builtin_tools()
17
+
18
+ def _add_project_to_path(self):
19
+ """Add the project root to Python path so we can import logic modules"""
20
+ # Get the directory containing this file (memra package)
21
+ current_dir = Path(__file__).parent
22
+ # Go up one level to get the project root
23
+ project_root = current_dir.parent
24
+
25
+ if str(project_root) not in sys.path:
26
+ sys.path.insert(0, str(project_root))
27
+
28
+ def _load_builtin_tools(self):
29
+ """Load tools from the logic directory"""
30
+ try:
31
+ # Load invoice tools
32
+ from logic.invoice_tools import (
33
+ DatabaseQueryTool, PDFProcessor, OCRTool,
34
+ InvoiceExtractionWorkflow, DataValidator, PostgresInsert
35
+ )
36
+
37
+ self.register_tool("DatabaseQueryTool", DatabaseQueryTool, "memra",
38
+ "Query database schemas and data")
39
+ self.register_tool("PDFProcessor", PDFProcessor, "memra",
40
+ "Process PDF files and extract content")
41
+ self.register_tool("OCRTool", OCRTool, "memra",
42
+ "Perform OCR on images and documents")
43
+ self.register_tool("InvoiceExtractionWorkflow", InvoiceExtractionWorkflow, "memra",
44
+ "Extract structured data from invoices")
45
+ self.register_tool("DataValidator", DataValidator, "memra",
46
+ "Validate data against schemas")
47
+ self.register_tool("PostgresInsert", PostgresInsert, "memra",
48
+ "Insert data into PostgreSQL database")
49
+
50
+ # Load file tools
51
+ from logic.file_tools import FileReader
52
+ self.register_tool("FileReader", FileReader, "memra",
53
+ "Read files from the filesystem")
54
+
55
+ logger.info(f"Loaded {len(self.tools)} builtin tools")
56
+
57
+ except ImportError as e:
58
+ logger.warning(f"Could not load some tools: {e}")
59
+
60
+ def register_tool(self, name: str, tool_class: type, hosted_by: str, description: str):
61
+ """Register a tool in the registry"""
62
+ self.tools[name] = {
63
+ "class": tool_class,
64
+ "hosted_by": hosted_by,
65
+ "description": description
66
+ }
67
+ logger.debug(f"Registered tool: {name} (hosted by {hosted_by})")
68
+
69
+ def discover_tools(self, hosted_by: Optional[str] = None) -> List[Dict[str, Any]]:
70
+ """Discover available tools, optionally filtered by host"""
71
+ tools = []
72
+ for name, info in self.tools.items():
73
+ if hosted_by is None or info["hosted_by"] == hosted_by:
74
+ tools.append({
75
+ "name": name,
76
+ "hosted_by": info["hosted_by"],
77
+ "description": info["description"]
78
+ })
79
+ return tools
80
+
81
+ def execute_tool(self, tool_name: str, hosted_by: str, input_data: Dict[str, Any],
82
+ config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
83
+ """Execute a tool with the given input data"""
84
+ if tool_name not in self.tools:
85
+ return {
86
+ "success": False,
87
+ "error": f"Tool '{tool_name}' not found in registry"
88
+ }
89
+
90
+ tool_info = self.tools[tool_name]
91
+ if tool_info["hosted_by"] != hosted_by:
92
+ return {
93
+ "success": False,
94
+ "error": f"Tool '{tool_name}' is hosted by '{tool_info['hosted_by']}', not '{hosted_by}'"
95
+ }
96
+
97
+ try:
98
+ # Instantiate tool
99
+ tool_class = tool_info["class"]
100
+
101
+ # Some tools need credentials/config for initialization
102
+ if tool_name in ["DatabaseQueryTool", "PostgresInsert"]:
103
+ if "connection" in input_data:
104
+ # Parse connection string or use credentials
105
+ credentials = self._parse_connection(input_data["connection"])
106
+ tool_instance = tool_class(credentials)
107
+ else:
108
+ return {
109
+ "success": False,
110
+ "error": f"Tool '{tool_name}' requires database credentials"
111
+ }
112
+ elif tool_name == "InvoiceExtractionWorkflow":
113
+ # This tool needs to be instantiated to initialize the LLM client
114
+ tool_instance = tool_class()
115
+ else:
116
+ tool_instance = tool_class()
117
+
118
+ # Execute tool based on its type
119
+ result = self._execute_tool_method(tool_instance, tool_name, input_data, config)
120
+
121
+ return {
122
+ "success": True,
123
+ "data": result
124
+ }
125
+
126
+ except Exception as e:
127
+ logger.error(f"Tool execution failed for {tool_name}: {str(e)}")
128
+ return {
129
+ "success": False,
130
+ "error": str(e)
131
+ }
132
+
133
+ def _execute_tool_method(self, tool_instance: Any, tool_name: str,
134
+ input_data: Dict[str, Any], config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
135
+ """Execute the appropriate method on the tool instance"""
136
+
137
+ if tool_name == "DatabaseQueryTool":
138
+ return tool_instance.get_schema("invoices") # Default to invoices table
139
+
140
+ elif tool_name == "PDFProcessor":
141
+ file_path = input_data.get("file", "")
142
+ return tool_instance.process_pdf(file_path)
143
+
144
+ elif tool_name == "OCRTool":
145
+ # Assume PDF processor output is passed as input
146
+ return {"extracted_text": tool_instance.extract_text(input_data)}
147
+
148
+ elif tool_name == "InvoiceExtractionWorkflow":
149
+ text = input_data.get("extracted_text", "")
150
+ schema = input_data.get("invoice_schema", {})
151
+ return tool_instance.extract_data(text, schema)
152
+
153
+ elif tool_name == "DataValidator":
154
+ data = input_data.get("invoice_data", {})
155
+ schema = input_data.get("invoice_schema", {})
156
+ return tool_instance.validate(data, schema)
157
+
158
+ elif tool_name == "PostgresInsert":
159
+ data = input_data.get("invoice_data", {})
160
+ return tool_instance.insert_record("invoices", data)
161
+
162
+ elif tool_name == "FileReader":
163
+ file_path = config.get("path") if config else input_data.get("file_path")
164
+ if not file_path:
165
+ raise ValueError("FileReader requires a file path")
166
+ return tool_instance.read_file(file_path)
167
+
168
+ else:
169
+ raise ValueError(f"Unknown tool execution method for {tool_name}")
170
+
171
+ def _parse_connection(self, connection_string: str) -> Dict[str, Any]:
172
+ """Parse a connection string into credentials"""
173
+ # Simple parser for postgres://user:pass@host:port/database
174
+ if connection_string.startswith("postgres://"):
175
+ # This is a simplified parser - in production you'd use a proper URL parser
176
+ parts = connection_string.replace("postgres://", "").split("/")
177
+ db_part = parts[1] if len(parts) > 1 else "finance"
178
+ auth_host = parts[0].split("@")
179
+ host_port = auth_host[1].split(":") if len(auth_host) > 1 else ["localhost", "5432"]
180
+ user_pass = auth_host[0].split(":") if len(auth_host) > 1 else ["user", "pass"]
181
+
182
+ return {
183
+ "host": host_port[0],
184
+ "port": int(host_port[1]) if len(host_port) > 1 else 5432,
185
+ "database": db_part,
186
+ "user": user_pass[0],
187
+ "password": user_pass[1] if len(user_pass) > 1 else ""
188
+ }
189
+
190
+ return {"connection_string": connection_string}
@@ -0,0 +1,106 @@
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://api.memra.co")
14
+ self.api_key = os.getenv("MEMRA_API_KEY")
15
+ self.tools_cache = None
16
+
17
+ if not self.api_key:
18
+ raise ValueError(
19
+ "MEMRA_API_KEY environment variable is required. "
20
+ "Please contact info@memra.co for an API key."
21
+ )
22
+
23
+ def discover_tools(self, hosted_by: Optional[str] = None) -> List[Dict[str, Any]]:
24
+ """Discover available tools from the API"""
25
+ try:
26
+ # Use sync httpx for compatibility with existing sync code
27
+ with httpx.Client(timeout=30.0) as client:
28
+ response = client.get(
29
+ f"{self.api_base}/tools/discover",
30
+ headers={"X-API-Key": self.api_key}
31
+ )
32
+ response.raise_for_status()
33
+
34
+ data = response.json()
35
+ tools = data.get("tools", [])
36
+
37
+ # Filter by hosted_by if specified
38
+ if hosted_by:
39
+ tools = [t for t in tools if t.get("hosted_by") == hosted_by]
40
+
41
+ self.tools_cache = tools
42
+ logger.info(f"Discovered {len(tools)} tools from API")
43
+ return tools
44
+
45
+ except Exception as e:
46
+ logger.error(f"Failed to discover tools from API: {e}")
47
+ # Return empty list if API is unavailable
48
+ return []
49
+
50
+ def execute_tool(self, tool_name: str, hosted_by: str, input_data: Dict[str, Any],
51
+ config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
52
+ """Execute a tool via the API"""
53
+ try:
54
+ logger.info(f"Executing tool {tool_name} via API")
55
+
56
+ # Prepare request payload
57
+ payload = {
58
+ "tool_name": tool_name,
59
+ "hosted_by": hosted_by,
60
+ "input_data": input_data,
61
+ "config": config
62
+ }
63
+
64
+ # Make API call
65
+ with httpx.Client(timeout=120.0) as client: # Longer timeout for tool execution
66
+ response = client.post(
67
+ f"{self.api_base}/tools/execute",
68
+ headers={
69
+ "X-API-Key": self.api_key,
70
+ "Content-Type": "application/json"
71
+ },
72
+ json=payload
73
+ )
74
+ response.raise_for_status()
75
+
76
+ result = response.json()
77
+ logger.info(f"Tool {tool_name} executed successfully via API")
78
+ return result
79
+
80
+ except httpx.TimeoutException:
81
+ logger.error(f"Tool {tool_name} execution timed out")
82
+ return {
83
+ "success": False,
84
+ "error": f"Tool execution timed out after 120 seconds"
85
+ }
86
+ except httpx.HTTPStatusError as e:
87
+ logger.error(f"API error for tool {tool_name}: {e.response.status_code}")
88
+ return {
89
+ "success": False,
90
+ "error": f"API error: {e.response.status_code} - {e.response.text}"
91
+ }
92
+ except Exception as e:
93
+ logger.error(f"Tool execution failed for {tool_name}: {str(e)}")
94
+ return {
95
+ "success": False,
96
+ "error": str(e)
97
+ }
98
+
99
+ def health_check(self) -> bool:
100
+ """Check if the API is available"""
101
+ try:
102
+ with httpx.Client(timeout=10.0) as client:
103
+ response = client.get(f"{self.api_base}/health")
104
+ return response.status_code == 200
105
+ except:
106
+ return False
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: memra
3
+ Version: 0.2.0
4
+ Summary: Declarative framework for enterprise workflows with MCP integration
5
+ Home-page: https://github.com/memra/memra-sdk
6
+ Author: Memra
7
+ Author-email: Memra <info@memra.co>
8
+ License: MIT
9
+ Project-URL: Homepage, https://memra.co
10
+ Project-URL: Repository, https://github.com/memra-platform/memra-sdk
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: pydantic>=1.8.0
24
+ Requires-Dist: httpx>=0.24.0
25
+ Requires-Dist: typing-extensions>=4.0.0
26
+ Requires-Dist: aiohttp>=3.8.0
27
+ Requires-Dist: aiohttp-cors>=0.7.0
28
+ Requires-Dist: psycopg2-binary>=2.9.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=6.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio; extra == "dev"
32
+ Requires-Dist: black; extra == "dev"
33
+ Requires-Dist: flake8; extra == "dev"
34
+ Dynamic: author
35
+ Dynamic: home-page
36
+ Dynamic: license-file
37
+ Dynamic: requires-python
38
+
39
+ # Memra SDK
40
+
41
+ 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.
42
+
43
+ ## 🚀 Team Setup
44
+
45
+ **New team member?** See the complete setup guide: **[TEAM_SETUP.md](TEAM_SETUP.md)**
46
+
47
+ This includes:
48
+ - Database setup (PostgreSQL + Docker)
49
+ - Local development environment
50
+ - Testing instructions
51
+ - Troubleshooting guide
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from memra.sdk.models import Agent, Department, Tool
57
+
58
+ # Define your agents
59
+ data_extractor = Agent(
60
+ role="Data Extraction Specialist",
61
+ job="Extract and validate data",
62
+ tools=[Tool(name="DataExtractor", hosted_by="memra")],
63
+ input_keys=["input_data"],
64
+ output_key="extracted_data"
65
+ )
66
+
67
+ # Create a department
68
+ dept = Department(
69
+ name="Data Processing",
70
+ mission="Process and validate data",
71
+ agents=[data_extractor]
72
+ )
73
+
74
+ # Run the workflow
75
+ result = dept.run({"input_data": {...}})
76
+ ```
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install memra
82
+ ```
83
+
84
+ ## API Access
85
+
86
+ Memra requires an API key to execute workflows on the hosted infrastructure.
87
+
88
+ ### Get Your API Key
89
+ Contact [info@memra.co](mailto:info@memra.co) for API access.
90
+
91
+ ### Set Your API Key
92
+ ```bash
93
+ # Set environment variable
94
+ export MEMRA_API_KEY="your-api-key-here"
95
+
96
+ # Or add to your shell profile for persistence
97
+ echo 'export MEMRA_API_KEY="your-api-key-here"' >> ~/.zshrc
98
+ ```
99
+
100
+ ### Test Your Setup
101
+ ```bash
102
+ python examples/accounts_payable_client.py
103
+ ```
104
+
105
+ ## Documentation
106
+
107
+ Documentation is coming soon. For now, see the examples below and in the `examples/` directory.
108
+
109
+ ## Example: Propane Delivery Workflow
110
+
111
+ See the `examples/propane_delivery.py` file for a complete example of how to use Memra to orchestrate a propane delivery workflow.
112
+
113
+ ## Contributing
114
+
115
+ We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) for details.
116
+
117
+ ## License
118
+
119
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
120
+
121
+ ## Examples
122
+
123
+ ```
124
+ ├── examples/
125
+ │ ├── accounts_payable_client.py # API-based example
126
+ │ ├── accounts_payable.py # Local example
127
+ │ ├── invoice_processing.py # Simple workflow
128
+ │ └── propane_delivery.py # Domain example
129
+ ├── memra/ # Core SDK
130
+ ├── logic/ # Tool implementations
131
+ ├── local/dependencies/ # Database setup & schemas
132
+ └── docker-compose.yml # Database setup
133
+ ```
134
+
135
+ ## ✨ New: MCP Integration
136
+
137
+ Memra now supports **Model Context Protocol (MCP)** integration, allowing you to execute operations on your local infrastructure while leveraging Memra's cloud-based AI processing.
138
+
139
+ **Key Benefits:**
140
+ - 🔒 **Keep sensitive data local** - Your databases stay on your infrastructure
141
+ - ⚡ **Hybrid processing** - AI processing in the cloud, data operations locally
142
+ - 🔐 **Secure communication** - HMAC-authenticated requests between cloud and local
143
+ - 🛠️ **Easy setup** - Simple bridge server connects your local resources
144
+
145
+ **Quick Example:**
146
+ ```python
147
+ # Agent that uses local database via MCP
148
+ agent = Agent(
149
+ role="Data Writer",
150
+ tools=[{
151
+ "name": "PostgresInsert",
152
+ "hosted_by": "mcp", # Routes to your local infrastructure
153
+ "config": {
154
+ "bridge_url": "http://localhost:8081",
155
+ "bridge_secret": "your-secret"
156
+ }
157
+ }]
158
+ )
159
+ ```
160
+
161
+ 📖 **[Complete MCP Integration Guide →](docs/mcp_integration.md)**
@@ -0,0 +1,19 @@
1
+ memra/__init__.py,sha256=XLSWpo42Ffp_pi5mvk4xvdYBZ8eNLAJF4_3Oi102i90,560
2
+ memra/discovery.py,sha256=yJIQnrDQu1nyzKykCIuzG_5SW5dIXHCEBLLKRWacIoY,480
3
+ memra/discovery_client.py,sha256=AbnKn6qhyrf7vmOvknEeDzH4tiGHsqPHtDaein_qaW0,1271
4
+ memra/execution.py,sha256=5NIyFVtQEeatYQ-fxexT0eWMtCh28k1hRC2Y6cfQaac,20917
5
+ memra/models.py,sha256=nTaYLAp0tRzQ0CQaBLNBURfhBQ5_gyty0ams4mghyIc,3289
6
+ memra/tool_registry.py,sha256=zdyKRShcmKtG7BVfmAHflW9FDl7rooPPAgbdVV4gJ8o,8268
7
+ memra/tool_registry_client.py,sha256=uzMQ4COvRams9vuPLcqcdljUpDlAYU_tyFxrRhrA0Lc,4009
8
+ memra-0.2.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ memra-sdk-package/examples/accounts_payable_client.py,sha256=Vu_h5C-qc6_80uz5dXJH4G3zfIbgUNAhQ2y8mWauao0,7401
10
+ memra-sdk-package/memra/__init__.py,sha256=QRk72YETLgL15GVt26tN_rBraCQkhZO7UB9T6d4u_uU,543
11
+ memra-sdk-package/memra/discovery_client.py,sha256=AbnKn6qhyrf7vmOvknEeDzH4tiGHsqPHtDaein_qaW0,1271
12
+ memra-sdk-package/memra/execution.py,sha256=UJ_MJ4getuSk4HJW1sCi7lc26avX-G6-GxnvE-DiSwk,20191
13
+ memra-sdk-package/memra/models.py,sha256=nTaYLAp0tRzQ0CQaBLNBURfhBQ5_gyty0ams4mghyIc,3289
14
+ memra-sdk-package/memra/tool_registry_client.py,sha256=KyNNxj84248E-8MoWNj6pJmlllUG8s0lmeXXmbu0U7o,3996
15
+ memra-0.2.0.dist-info/METADATA,sha256=eOuvH39VFUh-QxTdE5RwT6isgRIJkptEC2lsqlF2AA4,4816
16
+ memra-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ memra-0.2.0.dist-info/entry_points.txt,sha256=LBVjwWoxWJRzNLgeByPn6xUvWFIRnqnemvAZgIoSt08,41
18
+ memra-0.2.0.dist-info/top_level.txt,sha256=5dqePB77aj_pPFavlwxtBvdkUM-kP-WiQD3LRbQswwc,24
19
+ memra-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memra = memra.cli:main
@@ -0,0 +1,2 @@
1
+ memra
2
+ memra-sdk-package