proxima-sdk 1.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.
- proxima_sdk-1.0.0/.gitignore +35 -0
- proxima_sdk-1.0.0/PKG-INFO +57 -0
- proxima_sdk-1.0.0/README.md +37 -0
- proxima_sdk-1.0.0/__init__.py +50 -0
- proxima_sdk-1.0.0/context.py +140 -0
- proxima_sdk-1.0.0/exceptions.py +55 -0
- proxima_sdk-1.0.0/knowledge.py +269 -0
- proxima_sdk-1.0.0/pyproject.toml +34 -0
- proxima_sdk-1.0.0/testing.py +90 -0
- proxima_sdk-1.0.0/types.py +62 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Synthetic data (generated, ~1.3GB)
|
|
2
|
+
agents/supply-chain/demand-forecasting/data/synthetic/
|
|
3
|
+
|
|
4
|
+
# Python
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
|
|
13
|
+
# Environment
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Original reference docs (large PDFs, keep .md conversions only)
|
|
27
|
+
docs/reference/daimler/*.pdf
|
|
28
|
+
docs/reference/daimler/*.pptx
|
|
29
|
+
docs/reference/daimler/*.docx
|
|
30
|
+
docs/reference/daimler/*.msg
|
|
31
|
+
docs/reference/daimler/*.xlsx
|
|
32
|
+
docs/reference/daimler/*.zip
|
|
33
|
+
docs/reference/daimler/SAP Proposal - Supply Chain RFP/*.pdf
|
|
34
|
+
docs/reference/daimler/SAP Proposal - Supply Chain RFP/*.xlsx
|
|
35
|
+
platform/data/secrets.json
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxima-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Proxima AIP SDK — build AI agents on the Proxima Intelligence platform
|
|
5
|
+
Project-URL: Homepage, https://proximaintel.com/aip
|
|
6
|
+
Project-URL: Documentation, https://docs.proximaintel.com/sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/proximaintel/proxima-sdk
|
|
8
|
+
Author-email: Proxima Intelligence <hello@proximaintel.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,enterprise,platform,proxima
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Proxima SDK
|
|
22
|
+
|
|
23
|
+
The official Python SDK for building AI agents on the **Proxima AIP** platform.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install proxima-sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from proxima_sdk import PlatformContext
|
|
35
|
+
|
|
36
|
+
def my_tool(params: dict, ctx: PlatformContext) -> dict:
|
|
37
|
+
# Access knowledge bases
|
|
38
|
+
data = ctx.knowledge.query("What invoices are overdue?")
|
|
39
|
+
|
|
40
|
+
# Log actions for governance
|
|
41
|
+
ctx.governance.log_action("queried_invoices")
|
|
42
|
+
|
|
43
|
+
return {"answer": data.answer, "citations": data.citations}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Testing
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from proxima_sdk.testing import mock_context
|
|
50
|
+
|
|
51
|
+
ctx = mock_context(sources={"invoices": [{"id": "INV-001", "amount": 5000}]})
|
|
52
|
+
result = my_tool({"query": "overdue"}, ctx)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
Full docs at [docs.proximaintel.com/sdk](https://docs.proximaintel.com/sdk)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Proxima SDK
|
|
2
|
+
|
|
3
|
+
The official Python SDK for building AI agents on the **Proxima AIP** platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install proxima-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from proxima_sdk import PlatformContext
|
|
15
|
+
|
|
16
|
+
def my_tool(params: dict, ctx: PlatformContext) -> dict:
|
|
17
|
+
# Access knowledge bases
|
|
18
|
+
data = ctx.knowledge.query("What invoices are overdue?")
|
|
19
|
+
|
|
20
|
+
# Log actions for governance
|
|
21
|
+
ctx.governance.log_action("queried_invoices")
|
|
22
|
+
|
|
23
|
+
return {"answer": data.answer, "citations": data.citations}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from proxima_sdk.testing import mock_context
|
|
30
|
+
|
|
31
|
+
ctx = mock_context(sources={"invoices": [{"id": "INV-001", "amount": 5000}]})
|
|
32
|
+
result = my_tool({"query": "overdue"}, ctx)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Full docs at [docs.proximaintel.com/sdk](https://docs.proximaintel.com/sdk)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK
|
|
3
|
+
|
|
4
|
+
The official SDK for building toolboxes on the Proxima Intelligence platform.
|
|
5
|
+
Provides typed access to Knowledge, Governance, and Secrets services.
|
|
6
|
+
|
|
7
|
+
Quick Start:
|
|
8
|
+
from proxima_sdk import PlatformContext
|
|
9
|
+
|
|
10
|
+
def my_tool(params: dict, ctx: PlatformContext) -> dict:
|
|
11
|
+
data = ctx.knowledge.read("invoices-gold")
|
|
12
|
+
ctx.governance.log_action("processed_invoice")
|
|
13
|
+
return {"count": len(data)}
|
|
14
|
+
|
|
15
|
+
Testing:
|
|
16
|
+
from proxima_sdk.testing import mock_context
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
ctx = mock_context(sources={"invoices-gold": pd.DataFrame([...])})
|
|
20
|
+
result = my_tool({"id": "INV-001"}, ctx)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .context import PlatformContext
|
|
24
|
+
from .knowledge import KnowledgeClient
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
ProximaError,
|
|
27
|
+
SourceNotFoundError,
|
|
28
|
+
ConnectionFailedError,
|
|
29
|
+
QueryFailedError,
|
|
30
|
+
SecretNotFoundError,
|
|
31
|
+
PermissionDeniedError,
|
|
32
|
+
)
|
|
33
|
+
from .types import SearchResult, SearchResults, Citation, QueryMetadata
|
|
34
|
+
|
|
35
|
+
__version__ = "1.0.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"PlatformContext",
|
|
39
|
+
"KnowledgeClient",
|
|
40
|
+
"ProximaError",
|
|
41
|
+
"SourceNotFoundError",
|
|
42
|
+
"ConnectionFailedError",
|
|
43
|
+
"QueryFailedError",
|
|
44
|
+
"SecretNotFoundError",
|
|
45
|
+
"PermissionDeniedError",
|
|
46
|
+
"SearchResult",
|
|
47
|
+
"SearchResults",
|
|
48
|
+
"Citation",
|
|
49
|
+
"QueryMetadata",
|
|
50
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK — Platform Context
|
|
3
|
+
|
|
4
|
+
The single entry point injected into every toolbox tool call.
|
|
5
|
+
Contains everything the toolbox needs to interact with the platform.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from proxima_sdk import PlatformContext
|
|
9
|
+
|
|
10
|
+
def my_tool(params: dict, ctx: PlatformContext) -> dict:
|
|
11
|
+
data = ctx.knowledge.read("invoices-gold")
|
|
12
|
+
ctx.governance.log_action("validated_invoice", {"id": params["invoice_id"]})
|
|
13
|
+
return {"result": "pass"}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from .knowledge import KnowledgeClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GovernanceClient:
|
|
21
|
+
"""Log actions and events to the platform governance system."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, gateway_url: str, agent_id: str):
|
|
24
|
+
self._gateway_url = gateway_url
|
|
25
|
+
self._agent_id = agent_id
|
|
26
|
+
|
|
27
|
+
def log_action(self, action: str, detail: dict | None = None):
|
|
28
|
+
"""Log a toolbox action to governance (async fire-and-forget)."""
|
|
29
|
+
import httpx
|
|
30
|
+
try:
|
|
31
|
+
httpx.post(
|
|
32
|
+
f"{self._gateway_url}/governance/logs",
|
|
33
|
+
json={"agent_id": self._agent_id, "type": "action", "action": action, "detail": detail or {}},
|
|
34
|
+
timeout=5.0,
|
|
35
|
+
)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass # Non-blocking — governance logging should never break tool execution
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SecretsClient:
|
|
41
|
+
"""Resolve secrets from the platform secret store."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, gateway_url: str):
|
|
44
|
+
self._gateway_url = gateway_url
|
|
45
|
+
|
|
46
|
+
def resolve(self, name: str) -> str:
|
|
47
|
+
"""Resolve a secret value by name."""
|
|
48
|
+
import httpx
|
|
49
|
+
from .exceptions import SecretNotFoundError, ConnectionFailedError
|
|
50
|
+
try:
|
|
51
|
+
res = httpx.post(f"{self._gateway_url}/internal/secrets/resolve", json={"name": name}, timeout=5.0)
|
|
52
|
+
if res.status_code == 200:
|
|
53
|
+
return res.json().get("value", "")
|
|
54
|
+
elif res.status_code == 404:
|
|
55
|
+
raise SecretNotFoundError(name)
|
|
56
|
+
return ""
|
|
57
|
+
except httpx.ConnectError:
|
|
58
|
+
raise ConnectionFailedError("Gateway", self._gateway_url)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PlatformContext:
|
|
62
|
+
"""
|
|
63
|
+
Injected into every tool call. Provides access to all platform services.
|
|
64
|
+
|
|
65
|
+
Constructed by the gateway from the agent's config and passed to the toolbox
|
|
66
|
+
in the request payload under the `_context` key.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, config: dict):
|
|
70
|
+
"""
|
|
71
|
+
Args:
|
|
72
|
+
config: The _context dict injected by the gateway into tool call payloads.
|
|
73
|
+
{
|
|
74
|
+
"gateway_url": "https://gateway.example.com",
|
|
75
|
+
"agent_id": "invoice-intelligence",
|
|
76
|
+
"knowledge_bases": ["finance-kb"],
|
|
77
|
+
"sources": [...],
|
|
78
|
+
"token": "<_context HMAC token for dev mode>",
|
|
79
|
+
"identity": {
|
|
80
|
+
"client_id": "<agent SP client_id>",
|
|
81
|
+
"client_secret": "<agent SP secret>",
|
|
82
|
+
"tenant_id": "<IdP tenant>",
|
|
83
|
+
"audience": "api://<gateway app id>"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
"""
|
|
87
|
+
self._config = config
|
|
88
|
+
self._knowledge: Optional[KnowledgeClient] = None
|
|
89
|
+
self._governance: Optional[GovernanceClient] = None
|
|
90
|
+
self._secrets: Optional[SecretsClient] = None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def knowledge(self) -> KnowledgeClient:
|
|
94
|
+
"""Access enterprise data through the gateway."""
|
|
95
|
+
if self._knowledge is None:
|
|
96
|
+
identity = self._config.get("identity", {})
|
|
97
|
+
self._knowledge = KnowledgeClient(
|
|
98
|
+
gateway_url=self._config.get("gateway_url", "http://localhost:9000"),
|
|
99
|
+
knowledge_bases=self._config.get("knowledge_bases", []),
|
|
100
|
+
sources=self._config.get("sources", []),
|
|
101
|
+
token=self._config.get("token", ""),
|
|
102
|
+
client_id=identity.get("client_id", ""),
|
|
103
|
+
client_secret=identity.get("client_secret", ""),
|
|
104
|
+
tenant_id=identity.get("tenant_id", ""),
|
|
105
|
+
audience=identity.get("audience", ""),
|
|
106
|
+
)
|
|
107
|
+
return self._knowledge
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def governance(self) -> GovernanceClient:
|
|
111
|
+
"""Log actions to the platform governance system."""
|
|
112
|
+
if self._governance is None:
|
|
113
|
+
self._governance = GovernanceClient(
|
|
114
|
+
gateway_url=self._config.get("gateway_url", "http://localhost:9000"),
|
|
115
|
+
agent_id=self._config.get("agent_id", ""),
|
|
116
|
+
)
|
|
117
|
+
return self._governance
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def secrets(self) -> SecretsClient:
|
|
121
|
+
"""Resolve secrets from the platform secret store."""
|
|
122
|
+
if self._secrets is None:
|
|
123
|
+
self._secrets = SecretsClient(
|
|
124
|
+
gateway_url=self._config.get("gateway_url", "http://localhost:9000"),
|
|
125
|
+
)
|
|
126
|
+
return self._secrets
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def agent_id(self) -> str:
|
|
130
|
+
return self._config.get("agent_id", "")
|
|
131
|
+
|
|
132
|
+
def close(self):
|
|
133
|
+
if self._knowledge:
|
|
134
|
+
self._knowledge.close()
|
|
135
|
+
|
|
136
|
+
def __enter__(self):
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(self, *args):
|
|
140
|
+
self.close()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK — Exceptions
|
|
3
|
+
|
|
4
|
+
Clear, typed exceptions for toolbox developers.
|
|
5
|
+
Every error tells you what went wrong and how to fix it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProximaError(Exception):
|
|
10
|
+
"""Base exception for all Proxima SDK errors."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SourceNotFoundError(ProximaError):
|
|
15
|
+
"""Knowledge source not found in the connected knowledge base."""
|
|
16
|
+
def __init__(self, source_id: str):
|
|
17
|
+
super().__init__(f"Knowledge source '{source_id}' not found. Check that the source is registered and connected to the agent's knowledge base.")
|
|
18
|
+
self.source_id = source_id
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConnectionFailedError(ProximaError):
|
|
22
|
+
"""Failed to connect to a platform service (Knowledge Service, Gateway)."""
|
|
23
|
+
def __init__(self, service: str, url: str, detail: str = ""):
|
|
24
|
+
super().__init__(f"Failed to connect to {service} at {url}. {detail}")
|
|
25
|
+
self.service = service
|
|
26
|
+
self.url = url
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QueryFailedError(ProximaError):
|
|
30
|
+
"""Knowledge query failed — source unreachable or credentials invalid."""
|
|
31
|
+
def __init__(self, source_id: str, detail: str = ""):
|
|
32
|
+
super().__init__(f"Query failed for source '{source_id}'. {detail}")
|
|
33
|
+
self.source_id = source_id
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SecretNotFoundError(ProximaError):
|
|
37
|
+
"""Secret not found in the platform secret store."""
|
|
38
|
+
def __init__(self, secret_name: str):
|
|
39
|
+
super().__init__(f"Secret '{secret_name}' not found in platform secret store.")
|
|
40
|
+
self.secret_name = secret_name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PermissionDeniedError(ProximaError):
|
|
44
|
+
"""Insufficient permissions for the requested operation."""
|
|
45
|
+
def __init__(self, operation: str, detail: str = ""):
|
|
46
|
+
super().__init__(f"Permission denied for '{operation}'. {detail}")
|
|
47
|
+
self.operation = operation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TimeoutError(ProximaError):
|
|
51
|
+
"""Request timed out."""
|
|
52
|
+
def __init__(self, service: str, timeout_seconds: float):
|
|
53
|
+
super().__init__(f"Request to {service} timed out after {timeout_seconds}s.")
|
|
54
|
+
self.service = service
|
|
55
|
+
self.timeout_seconds = timeout_seconds
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK — Knowledge Client
|
|
3
|
+
|
|
4
|
+
The primary interface for toolboxes to access enterprise data.
|
|
5
|
+
All access goes through the gateway (authenticated, RBAC-enforced, governance-logged).
|
|
6
|
+
|
|
7
|
+
The toolbox NEVER calls Knowledge Service directly. It authenticates to the gateway
|
|
8
|
+
using the agent's service principal credentials and calls POST /knowledge/{base_id}/query.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from proxima_sdk import PlatformContext
|
|
12
|
+
|
|
13
|
+
def validate_invoice(params: dict, ctx: PlatformContext) -> dict:
|
|
14
|
+
results = ctx.knowledge.query("finance-kb", "overdue invoices over $50K")
|
|
15
|
+
invoices = ctx.knowledge.read("invoices-gold")
|
|
16
|
+
# ... business logic
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
from .types import SearchResult, SearchResults, Citation, QueryMetadata
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
SourceNotFoundError,
|
|
29
|
+
ConnectionFailedError,
|
|
30
|
+
QueryFailedError,
|
|
31
|
+
TimeoutError as ProximaTimeoutError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class KnowledgeClient:
|
|
36
|
+
"""
|
|
37
|
+
Access enterprise data through the Proxima Gateway.
|
|
38
|
+
|
|
39
|
+
Provides:
|
|
40
|
+
- query(base_id, query) → dict (full KB query via gateway)
|
|
41
|
+
- read(source_id) → pd.DataFrame (structured data via gateway)
|
|
42
|
+
- search(source_id, query) → SearchResults (unstructured/vector via gateway)
|
|
43
|
+
|
|
44
|
+
All access authenticated, RBAC-enforced, governance-logged.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, gateway_url: str, knowledge_bases: list[str], sources: list[dict],
|
|
48
|
+
token: str = "", client_id: str = "", client_secret: str = "",
|
|
49
|
+
tenant_id: str = "", audience: str = "", timeout: float = 30.0):
|
|
50
|
+
"""
|
|
51
|
+
Args:
|
|
52
|
+
gateway_url: Platform gateway URL (e.g., https://gateway.example.com)
|
|
53
|
+
knowledge_bases: List of KB IDs this agent can access
|
|
54
|
+
sources: Legacy source definitions (for backward compat with read())
|
|
55
|
+
token: Pre-obtained token (dev mode / _context HMAC)
|
|
56
|
+
client_id: Agent SP client ID (production - client credentials flow)
|
|
57
|
+
client_secret: Agent SP client secret (production)
|
|
58
|
+
tenant_id: IdP tenant ID (for token endpoint)
|
|
59
|
+
audience: Gateway audience (for token scope)
|
|
60
|
+
timeout: HTTP timeout in seconds
|
|
61
|
+
"""
|
|
62
|
+
self._gateway_url = gateway_url.rstrip("/")
|
|
63
|
+
self._knowledge_bases = knowledge_bases
|
|
64
|
+
self._sources = {s["id"]: s for s in sources}
|
|
65
|
+
self._timeout = timeout
|
|
66
|
+
self._cache: dict[str, pd.DataFrame] = {}
|
|
67
|
+
|
|
68
|
+
# Auth
|
|
69
|
+
self._token = token
|
|
70
|
+
self._client_id = client_id or os.getenv("PROXIMA_AGENT_CLIENT_ID", "")
|
|
71
|
+
self._client_secret = client_secret or os.getenv("PROXIMA_AGENT_CLIENT_SECRET", "")
|
|
72
|
+
self._tenant_id = tenant_id or os.getenv("PROXIMA_TENANT_ID", "")
|
|
73
|
+
self._audience = audience or os.getenv("PROXIMA_AUDIENCE", "")
|
|
74
|
+
self._token_expires_at: float = 0
|
|
75
|
+
|
|
76
|
+
def _get_token(self) -> str:
|
|
77
|
+
"""Get a valid access token. Uses cached token if not expired."""
|
|
78
|
+
# If we have a pre-set token (dev mode / _context), use it
|
|
79
|
+
if self._token and not self._client_id:
|
|
80
|
+
return self._token
|
|
81
|
+
|
|
82
|
+
# Check if cached token is still valid (with 60s buffer)
|
|
83
|
+
if self._token and time.time() < self._token_expires_at - 60:
|
|
84
|
+
return self._token
|
|
85
|
+
|
|
86
|
+
# Client credentials flow
|
|
87
|
+
if not self._client_id or not self._client_secret or not self._tenant_id:
|
|
88
|
+
return self._token # Fall back to whatever we have
|
|
89
|
+
|
|
90
|
+
token_url = f"https://login.microsoftonline.com/{self._tenant_id}/oauth2/v2.0/token"
|
|
91
|
+
scope = f"{self._audience}/.default" if self._audience else ""
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
res = httpx.post(token_url, data={
|
|
95
|
+
"client_id": self._client_id,
|
|
96
|
+
"client_secret": self._client_secret,
|
|
97
|
+
"scope": scope,
|
|
98
|
+
"grant_type": "client_credentials",
|
|
99
|
+
}, timeout=10.0)
|
|
100
|
+
|
|
101
|
+
if res.status_code == 200:
|
|
102
|
+
data = res.json()
|
|
103
|
+
self._token = data["access_token"]
|
|
104
|
+
self._token_expires_at = time.time() + data.get("expires_in", 3600)
|
|
105
|
+
return self._token
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return self._token
|
|
110
|
+
|
|
111
|
+
def _headers(self) -> dict:
|
|
112
|
+
"""Build request headers with auth token."""
|
|
113
|
+
token = self._get_token()
|
|
114
|
+
headers = {"Content-Type": "application/json"}
|
|
115
|
+
if token:
|
|
116
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
117
|
+
return headers
|
|
118
|
+
|
|
119
|
+
def query(self, base_id: str, query: str, top_k: int = 5) -> dict:
|
|
120
|
+
"""
|
|
121
|
+
Query a knowledge base through the gateway.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
base_id: Knowledge base ID
|
|
125
|
+
query: Natural language query
|
|
126
|
+
top_k: Number of results
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict with results, citations, metadata
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
QueryFailedError: Gateway returned an error
|
|
133
|
+
ConnectionFailedError: Gateway unreachable
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
start = time.time()
|
|
137
|
+
res = httpx.post(
|
|
138
|
+
f"{self._gateway_url}/knowledge/{base_id}/query",
|
|
139
|
+
json={"query": query, "top_k": top_k},
|
|
140
|
+
headers=self._headers(),
|
|
141
|
+
timeout=self._timeout,
|
|
142
|
+
)
|
|
143
|
+
duration = int((time.time() - start) * 1000)
|
|
144
|
+
|
|
145
|
+
if res.status_code == 403:
|
|
146
|
+
raise QueryFailedError(base_id, "Access denied. Check role_assignments for this agent.")
|
|
147
|
+
if res.status_code == 404:
|
|
148
|
+
raise SourceNotFoundError(base_id)
|
|
149
|
+
if res.status_code != 200:
|
|
150
|
+
raise QueryFailedError(base_id, f"HTTP {res.status_code}: {res.text[:200]}")
|
|
151
|
+
|
|
152
|
+
return res.json()
|
|
153
|
+
|
|
154
|
+
except httpx.ConnectError:
|
|
155
|
+
raise ConnectionFailedError("Gateway", self._gateway_url, "Gateway unreachable.")
|
|
156
|
+
except httpx.TimeoutException:
|
|
157
|
+
raise ProximaTimeoutError("Gateway", self._timeout)
|
|
158
|
+
|
|
159
|
+
def read(self, source_id: str, use_cache: bool = True) -> pd.DataFrame:
|
|
160
|
+
"""
|
|
161
|
+
Read structured data from a knowledge source.
|
|
162
|
+
Routes through gateway /knowledge/{base_id}/query.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
source_id: ID of the registered knowledge source
|
|
166
|
+
use_cache: If True, returns cached data within same tool execution
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
pd.DataFrame with the source data
|
|
170
|
+
"""
|
|
171
|
+
if use_cache and source_id in self._cache:
|
|
172
|
+
return self._cache[source_id]
|
|
173
|
+
|
|
174
|
+
# Find which KB contains this source
|
|
175
|
+
# For now, query the first available KB with the source name
|
|
176
|
+
base_id = self._knowledge_bases[0] if self._knowledge_bases else "default"
|
|
177
|
+
|
|
178
|
+
result = self.query(base_id, f"read all data from {source_id}", top_k=1)
|
|
179
|
+
|
|
180
|
+
# Parse results into DataFrame
|
|
181
|
+
for r in result.get("results", []):
|
|
182
|
+
content = r.get("content", "")
|
|
183
|
+
if "rows" in content and "|" in content:
|
|
184
|
+
# Try to parse markdown table
|
|
185
|
+
try:
|
|
186
|
+
lines = [l.strip() for l in content.split("\n") if l.strip() and "|" in l]
|
|
187
|
+
if len(lines) >= 3:
|
|
188
|
+
headers = [h.strip() for h in lines[0].split("|") if h.strip()]
|
|
189
|
+
rows = []
|
|
190
|
+
for line in lines[2:]:
|
|
191
|
+
if "---" in line or "..." in line:
|
|
192
|
+
continue
|
|
193
|
+
row = [c.strip() for c in line.split("|") if c.strip()]
|
|
194
|
+
if len(row) == len(headers):
|
|
195
|
+
rows.append(row)
|
|
196
|
+
if rows:
|
|
197
|
+
df = pd.DataFrame(rows, columns=headers)
|
|
198
|
+
if use_cache:
|
|
199
|
+
self._cache[source_id] = df
|
|
200
|
+
return df
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# Fallback: return empty DataFrame
|
|
205
|
+
df = pd.DataFrame()
|
|
206
|
+
if use_cache:
|
|
207
|
+
self._cache[source_id] = df
|
|
208
|
+
return df
|
|
209
|
+
|
|
210
|
+
def search(self, source_id: str, query: str, top_k: int = 5) -> SearchResults:
|
|
211
|
+
"""
|
|
212
|
+
Search knowledge via gateway.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
source_id: Source or KB ID to search
|
|
216
|
+
query: Natural language search query
|
|
217
|
+
top_k: Number of results
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
SearchResults with content and citations
|
|
221
|
+
"""
|
|
222
|
+
base_id = source_id if source_id in self._knowledge_bases else (self._knowledge_bases[0] if self._knowledge_bases else source_id)
|
|
223
|
+
|
|
224
|
+
result = self.query(base_id, query, top_k=top_k)
|
|
225
|
+
|
|
226
|
+
results = []
|
|
227
|
+
for r in result.get("results", []):
|
|
228
|
+
citation_data = r.get("citation", {})
|
|
229
|
+
results.append(SearchResult(
|
|
230
|
+
content=r.get("content", ""),
|
|
231
|
+
citation=Citation(
|
|
232
|
+
source=citation_data.get("source", ""),
|
|
233
|
+
title=citation_data.get("title", ""),
|
|
234
|
+
confidence=citation_data.get("confidence", 0.0),
|
|
235
|
+
metadata=citation_data,
|
|
236
|
+
),
|
|
237
|
+
score=0.0,
|
|
238
|
+
))
|
|
239
|
+
|
|
240
|
+
return SearchResults(
|
|
241
|
+
results=results,
|
|
242
|
+
query=query,
|
|
243
|
+
sources_queried=result.get("metadata", {}).get("sources_queried", 0),
|
|
244
|
+
duration_ms=result.get("metadata", {}).get("duration_ms", 0),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def available_bases(self) -> list[str]:
|
|
249
|
+
"""List KB IDs available to this agent."""
|
|
250
|
+
return list(self._knowledge_bases)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def available_sources(self) -> list[str]:
|
|
254
|
+
"""List source IDs (legacy compat)."""
|
|
255
|
+
return list(self._sources.keys())
|
|
256
|
+
|
|
257
|
+
def clear_cache(self):
|
|
258
|
+
"""Clear the in-memory read cache."""
|
|
259
|
+
self._cache.clear()
|
|
260
|
+
|
|
261
|
+
def close(self):
|
|
262
|
+
"""No persistent client to close (stateless HTTP calls)."""
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def __enter__(self):
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
def __exit__(self, *args):
|
|
269
|
+
self.close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proxima-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Proxima AIP SDK — build AI agents on the Proxima Intelligence platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Proxima Intelligence", email = "hello@proximaintel.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agents", "platform", "enterprise", "proxima"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.27",
|
|
25
|
+
"pydantic>=2.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://proximaintel.com/aip"
|
|
30
|
+
Documentation = "https://docs.proximaintel.com/sdk"
|
|
31
|
+
Repository = "https://github.com/proximaintel/proxima-sdk"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["proxima_sdk"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK — Testing Utilities
|
|
3
|
+
|
|
4
|
+
Mock clients for unit testing toolbox code without the full platform stack.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from proxima_sdk.testing import mock_context
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
def test_validate_invoice():
|
|
11
|
+
ctx = mock_context(sources={
|
|
12
|
+
"invoices-gold": pd.DataFrame([{"invoice_id": "INV-001", "amount": 1000}]),
|
|
13
|
+
"vendors-gold": pd.DataFrame([{"vendor_id": "V-001", "name": "Acme"}]),
|
|
14
|
+
})
|
|
15
|
+
result = validate_invoice({"invoice_id": "INV-001"}, ctx)
|
|
16
|
+
assert result["validation"] == "pass"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Optional
|
|
20
|
+
import pandas as pd
|
|
21
|
+
from .types import SearchResult, SearchResults, Citation
|
|
22
|
+
from .context import PlatformContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MockKnowledgeClient:
|
|
26
|
+
"""Mock knowledge client for testing. Returns pre-loaded DataFrames."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, sources: dict[str, pd.DataFrame] | None = None, search_results: dict[str, list[dict]] | None = None):
|
|
29
|
+
self._sources = sources or {}
|
|
30
|
+
self._search_results = search_results or {}
|
|
31
|
+
|
|
32
|
+
def read(self, source_id: str, use_cache: bool = True) -> pd.DataFrame:
|
|
33
|
+
if source_id not in self._sources:
|
|
34
|
+
from .exceptions import SourceNotFoundError
|
|
35
|
+
raise SourceNotFoundError(source_id)
|
|
36
|
+
return self._sources[source_id]
|
|
37
|
+
|
|
38
|
+
def search(self, source_id: str, query: str, top_k: int = 5) -> SearchResults:
|
|
39
|
+
results = self._search_results.get(source_id, [])
|
|
40
|
+
return SearchResults(
|
|
41
|
+
results=[SearchResult(content=r.get("content", ""), citation=Citation(source=source_id, title=r.get("title", "")), score=r.get("score", 0.9)) for r in results[:top_k]],
|
|
42
|
+
query=query,
|
|
43
|
+
sources_queried=1,
|
|
44
|
+
duration_ms=1,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def available_sources(self) -> list[str]:
|
|
49
|
+
return list(self._sources.keys())
|
|
50
|
+
|
|
51
|
+
def clear_cache(self):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def close(self):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MockGovernanceClient:
|
|
59
|
+
"""Mock governance client — records actions for assertion."""
|
|
60
|
+
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self.actions: list[dict] = []
|
|
63
|
+
|
|
64
|
+
def log_action(self, action: str, detail: dict | None = None):
|
|
65
|
+
self.actions.append({"action": action, "detail": detail or {}})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MockSecretsClient:
|
|
69
|
+
"""Mock secrets client — returns pre-configured values."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, secrets: dict[str, str] | None = None):
|
|
72
|
+
self._secrets = secrets or {}
|
|
73
|
+
|
|
74
|
+
def resolve(self, name: str) -> str:
|
|
75
|
+
return self._secrets.get(name, "")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MockPlatformContext(PlatformContext):
|
|
79
|
+
"""Mock platform context for testing."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, sources: dict[str, pd.DataFrame] | None = None, search_results: dict[str, list[dict]] | None = None, secrets: dict[str, str] | None = None):
|
|
82
|
+
super().__init__({"agent_id": "test-agent", "knowledge_service_url": "", "gateway_url": "", "sources": []})
|
|
83
|
+
self._knowledge = MockKnowledgeClient(sources=sources, search_results=search_results)
|
|
84
|
+
self._governance = MockGovernanceClient()
|
|
85
|
+
self._secrets = MockSecretsClient(secrets=secrets)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mock_context(sources: dict[str, pd.DataFrame] | None = None, search_results: dict[str, list[dict]] | None = None, secrets: dict[str, str] | None = None) -> MockPlatformContext:
|
|
89
|
+
"""Create a mock PlatformContext for unit testing."""
|
|
90
|
+
return MockPlatformContext(sources=sources, search_results=search_results, secrets=secrets)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxima SDK — Types
|
|
3
|
+
|
|
4
|
+
Typed data structures for toolbox developers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Citation:
|
|
13
|
+
"""Source attribution for a knowledge retrieval result."""
|
|
14
|
+
source: str
|
|
15
|
+
title: str = ""
|
|
16
|
+
section: str = ""
|
|
17
|
+
page: Optional[int] = None
|
|
18
|
+
url: str = ""
|
|
19
|
+
confidence: float = 0.0
|
|
20
|
+
metadata: dict = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SearchResult:
|
|
25
|
+
"""A single result from a knowledge search (unstructured/vector)."""
|
|
26
|
+
content: str
|
|
27
|
+
citation: Citation
|
|
28
|
+
score: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SearchResults:
|
|
33
|
+
"""Collection of search results with metadata."""
|
|
34
|
+
results: list[SearchResult]
|
|
35
|
+
query: str
|
|
36
|
+
sources_queried: int = 0
|
|
37
|
+
duration_ms: int = 0
|
|
38
|
+
|
|
39
|
+
def __len__(self) -> int:
|
|
40
|
+
return len(self.results)
|
|
41
|
+
|
|
42
|
+
def __iter__(self):
|
|
43
|
+
return iter(self.results)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def top(self) -> Optional[SearchResult]:
|
|
47
|
+
return self.results[0] if self.results else None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def contents(self) -> list[str]:
|
|
51
|
+
return [r.content for r in self.results]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class QueryMetadata:
|
|
56
|
+
"""Metadata about a structured data query."""
|
|
57
|
+
source: str
|
|
58
|
+
row_count: int
|
|
59
|
+
columns: list[str]
|
|
60
|
+
query_executed: str = ""
|
|
61
|
+
duration_ms: int = 0
|
|
62
|
+
cached: bool = False
|