agentic-fabriq-sdk 0.1.3__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.

Potentially problematic release.


This version of agentic-fabriq-sdk might be problematic. Click here for more details.

@@ -0,0 +1,23 @@
1
+ """
2
+ Connector framework for Agentic Fabric SDK.
3
+ """
4
+
5
+ from .base import (
6
+ AgentConnector,
7
+ BaseConnector,
8
+ ConnectorContext,
9
+ HTTPConnectorMixin,
10
+ MCPConnector,
11
+ ToolConnector,
12
+ )
13
+ from .registry import ConnectorRegistry
14
+
15
+ __all__ = [
16
+ "BaseConnector",
17
+ "ToolConnector",
18
+ "AgentConnector",
19
+ "MCPConnector",
20
+ "ConnectorContext",
21
+ "HTTPConnectorMixin",
22
+ "ConnectorRegistry",
23
+ ]
@@ -0,0 +1,231 @@
1
+ """
2
+ Base connector classes for Agentic Fabric SDK.
3
+ """
4
+
5
+ import abc
6
+ import logging
7
+ from typing import Any, Dict, Optional
8
+
9
+ import httpx
10
+ from pydantic import BaseModel
11
+
12
+ from ..auth.token_cache import TokenManager
13
+ from ..exceptions import ConnectorError
14
+
15
+
16
+ class ConnectorContext(BaseModel):
17
+ """Context object passed to connector instances."""
18
+
19
+ tenant_id: str
20
+ user_id: Optional[str] = None
21
+ http: httpx.AsyncClient
22
+ token_manager: "TokenManager"
23
+ logger: logging.Logger
24
+ metadata: Dict[str, Any] = {}
25
+
26
+ class Config:
27
+ arbitrary_types_allowed = True
28
+
29
+
30
+ class BaseConnector(abc.ABC):
31
+ """Base class for all connectors."""
32
+
33
+ def __init__(self, ctx: ConnectorContext):
34
+ self.ctx = ctx
35
+ self.session = ctx.http
36
+ self.logger = ctx.logger
37
+ self.token_manager = ctx.token_manager
38
+
39
+ @abc.abstractmethod
40
+ async def invoke(self, method: str, **kwargs) -> Any:
41
+ """Generic fallback invocation method."""
42
+ pass
43
+
44
+ async def health_check(self) -> Dict[str, Any]:
45
+ """Health check for the connector."""
46
+ return {"status": "healthy", "connector": self.__class__.__name__}
47
+
48
+ def get_metadata(self) -> Dict[str, Any]:
49
+ """Get connector metadata."""
50
+ return {
51
+ "name": self.__class__.__name__,
52
+ "version": getattr(self, "__version__", "unknown"),
53
+ "description": self.__doc__ or "No description available",
54
+ }
55
+
56
+
57
+ class ToolConnector(BaseConnector):
58
+ """Base class for tool connectors."""
59
+
60
+ # Override in subclass
61
+ TOOL_ID: str = ""
62
+
63
+ def __init__(self, ctx: ConnectorContext):
64
+ super().__init__(ctx)
65
+ if not self.TOOL_ID:
66
+ raise ConnectorError("TOOL_ID must be set in connector subclass")
67
+
68
+ @abc.abstractmethod
69
+ async def invoke(self, method: str, **kwargs) -> Any:
70
+ """
71
+ Invoke a tool method.
72
+
73
+ Args:
74
+ method: The tool method to invoke
75
+ **kwargs: Method arguments
76
+
77
+ Returns:
78
+ The result of the tool invocation
79
+ """
80
+ pass
81
+
82
+ async def get_schema(self) -> Dict[str, Any]:
83
+ """Get the tool schema/specification."""
84
+ return {
85
+ "tool_id": self.TOOL_ID,
86
+ "methods": self._get_available_methods(),
87
+ "auth_required": True,
88
+ "scopes": getattr(self, "REQUIRED_SCOPES", []),
89
+ }
90
+
91
+ def _get_available_methods(self) -> list[str]:
92
+ """Get list of available methods for this tool."""
93
+ methods = []
94
+ for attr_name in dir(self):
95
+ if not attr_name.startswith("_") and callable(getattr(self, attr_name)):
96
+ attr = getattr(self, attr_name)
97
+ if hasattr(attr, "__annotations__") and not attr_name in [
98
+ "invoke",
99
+ "health_check",
100
+ "get_metadata",
101
+ "get_schema",
102
+ ]:
103
+ methods.append(attr_name)
104
+ return methods
105
+
106
+
107
+ class AgentConnector(BaseConnector):
108
+ """Base class for agent connectors that speak A2A or MCP."""
109
+
110
+ AGENT_ID: str = ""
111
+
112
+ def __init__(self, ctx: ConnectorContext):
113
+ super().__init__(ctx)
114
+ if not self.AGENT_ID:
115
+ raise ConnectorError("AGENT_ID must be set in connector subclass")
116
+
117
+ @abc.abstractmethod
118
+ async def invoke(self, input_text: str, **params) -> str:
119
+ """
120
+ Invoke the agent.
121
+
122
+ Args:
123
+ input_text: The input text/prompt for the agent
124
+ **params: Additional parameters for the agent
125
+
126
+ Returns:
127
+ The agent's response
128
+ """
129
+ pass
130
+
131
+ async def get_capabilities(self) -> Dict[str, Any]:
132
+ """Get agent capabilities."""
133
+ return {
134
+ "agent_id": self.AGENT_ID,
135
+ "supports_streaming": getattr(self, "SUPPORTS_STREAMING", False),
136
+ "max_tokens": getattr(self, "MAX_TOKENS", None),
137
+ "supported_models": getattr(self, "SUPPORTED_MODELS", []),
138
+ }
139
+
140
+ async def validate_input(self, input_text: str, **params) -> bool:
141
+ """Validate input before processing."""
142
+ if not input_text or not input_text.strip():
143
+ return False
144
+ return True
145
+
146
+
147
+ class MCPConnector(BaseConnector):
148
+ """Base class for MCP (Model Context Protocol) connectors."""
149
+
150
+ MCP_VERSION: str = "1.0"
151
+ SERVER_NAME: str = ""
152
+
153
+ def __init__(self, ctx: ConnectorContext):
154
+ super().__init__(ctx)
155
+ if not self.SERVER_NAME:
156
+ raise ConnectorError("SERVER_NAME must be set in MCP connector subclass")
157
+
158
+ @abc.abstractmethod
159
+ async def list_tools(self) -> list[Dict[str, Any]]:
160
+ """List available tools from the MCP server."""
161
+ pass
162
+
163
+ @abc.abstractmethod
164
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
165
+ """Call a specific tool on the MCP server."""
166
+ pass
167
+
168
+ async def invoke(self, method: str, **kwargs) -> Any:
169
+ """Generic invoke method for MCP connectors."""
170
+ if method == "list_tools":
171
+ return await self.list_tools()
172
+ elif method == "call_tool":
173
+ return await self.call_tool(
174
+ kwargs.get("tool_name", ""), kwargs.get("arguments", {})
175
+ )
176
+ else:
177
+ raise ConnectorError(f"Unknown method: {method}")
178
+
179
+ async def get_server_info(self) -> Dict[str, Any]:
180
+ """Get MCP server information."""
181
+ return {
182
+ "name": self.SERVER_NAME,
183
+ "version": self.MCP_VERSION,
184
+ "tools": await self.list_tools(),
185
+ }
186
+
187
+
188
+ class HTTPConnectorMixin:
189
+ """Mixin for connectors that make HTTP requests."""
190
+
191
+ def __init__(self, *args, **kwargs):
192
+ super().__init__(*args, **kwargs)
193
+ self.base_url = kwargs.get("base_url", "")
194
+ self.default_headers = kwargs.get("default_headers", {})
195
+
196
+ async def _make_request(
197
+ self,
198
+ method: str,
199
+ path: str,
200
+ headers: Optional[Dict[str, str]] = None,
201
+ **kwargs,
202
+ ) -> httpx.Response:
203
+ """Make an HTTP request with proper error handling."""
204
+ url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
205
+ request_headers = {**self.default_headers, **(headers or {})}
206
+
207
+ try:
208
+ response = await self.session.request(
209
+ method, url, headers=request_headers, **kwargs
210
+ )
211
+ response.raise_for_status()
212
+ return response
213
+ except httpx.HTTPError as e:
214
+ self.logger.error(f"HTTP request failed: {e}")
215
+ raise ConnectorError(f"HTTP request failed: {e}")
216
+
217
+ async def _get(self, path: str, **kwargs) -> httpx.Response:
218
+ """Make a GET request."""
219
+ return await self._make_request("GET", path, **kwargs)
220
+
221
+ async def _post(self, path: str, **kwargs) -> httpx.Response:
222
+ """Make a POST request."""
223
+ return await self._make_request("POST", path, **kwargs)
224
+
225
+ async def _put(self, path: str, **kwargs) -> httpx.Response:
226
+ """Make a PUT request."""
227
+ return await self._make_request("PUT", path, **kwargs)
228
+
229
+ async def _delete(self, path: str, **kwargs) -> httpx.Response:
230
+ """Make a DELETE request."""
231
+ return await self._make_request("DELETE", path, **kwargs)
@@ -0,0 +1,262 @@
1
+ """
2
+ Connector registry for plugin discovery and management.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Dict, List, Optional, Type
7
+
8
+ from stevedore import driver, named
9
+
10
+ from ..exceptions import ConnectorError
11
+ from .base import BaseConnector, ConnectorContext
12
+
13
+
14
+ class ConnectorRegistry:
15
+ """Registry for managing connector plugins."""
16
+
17
+ def __init__(self, logger: Optional[logging.Logger] = None):
18
+ self.logger = logger or logging.getLogger(__name__)
19
+ self._loaded_connectors: Dict[str, Type[BaseConnector]] = {}
20
+
21
+ def load_connector(self, connector_id: str) -> Type[BaseConnector]:
22
+ """
23
+ Load a connector by ID.
24
+
25
+ Args:
26
+ connector_id: The connector ID (e.g., 'slack', 'github')
27
+
28
+ Returns:
29
+ The connector class
30
+
31
+ Raises:
32
+ ConnectorError: If connector cannot be loaded
33
+ """
34
+ if connector_id in self._loaded_connectors:
35
+ return self._loaded_connectors[connector_id]
36
+
37
+ try:
38
+ # Load via stevedore
39
+ mgr = driver.DriverManager(
40
+ namespace="af_connector_plugins",
41
+ name=connector_id,
42
+ invoke_on_load=False,
43
+ )
44
+ connector_class = mgr.driver
45
+
46
+ # Validate connector class
47
+ if not issubclass(connector_class, BaseConnector):
48
+ raise ConnectorError(
49
+ f"Connector {connector_id} must inherit from BaseConnector"
50
+ )
51
+
52
+ self._loaded_connectors[connector_id] = connector_class
53
+ self.logger.info(f"Loaded connector: {connector_id}")
54
+ return connector_class
55
+
56
+ except Exception as e:
57
+ self.logger.error(f"Failed to load connector {connector_id}: {e}")
58
+ raise ConnectorError(f"Failed to load connector {connector_id}: {e}")
59
+
60
+ def list_available_connectors(self) -> List[str]:
61
+ """
62
+ List all available connector IDs.
63
+
64
+ Returns:
65
+ List of connector IDs
66
+ """
67
+ try:
68
+ from stevedore import extension
69
+ mgr = extension.ExtensionManager(
70
+ namespace="af_connector_plugins",
71
+ invoke_on_load=False,
72
+ )
73
+ return [ep.name for ep in mgr.extensions]
74
+ except Exception as e:
75
+ self.logger.error(f"Failed to list connectors: {e}")
76
+ return []
77
+
78
+ def get_connector_info(self, connector_id: str) -> Dict[str, Any]:
79
+ """
80
+ Get information about a connector.
81
+
82
+ Args:
83
+ connector_id: The connector ID
84
+
85
+ Returns:
86
+ Connector information
87
+ """
88
+ try:
89
+ connector_class = self.load_connector(connector_id)
90
+ return {
91
+ "id": connector_id,
92
+ "name": connector_class.__name__,
93
+ "description": connector_class.__doc__ or "No description",
94
+ "version": getattr(connector_class, "__version__", "unknown"),
95
+ "tool_id": getattr(connector_class, "TOOL_ID", None),
96
+ "agent_id": getattr(connector_class, "AGENT_ID", None),
97
+ "required_scopes": getattr(connector_class, "REQUIRED_SCOPES", []),
98
+ }
99
+ except Exception as e:
100
+ return {
101
+ "id": connector_id,
102
+ "error": str(e),
103
+ }
104
+
105
+ def create_connector_instance(
106
+ self, connector_id: str, context: ConnectorContext
107
+ ) -> BaseConnector:
108
+ """
109
+ Create a connector instance.
110
+
111
+ Args:
112
+ connector_id: The connector ID
113
+ context: Connector context
114
+
115
+ Returns:
116
+ Connector instance
117
+
118
+ Raises:
119
+ ConnectorError: If connector cannot be created
120
+ """
121
+ try:
122
+ connector_class = self.load_connector(connector_id)
123
+ return connector_class(context)
124
+ except Exception as e:
125
+ self.logger.error(f"Failed to create connector {connector_id}: {e}")
126
+ raise ConnectorError(f"Failed to create connector {connector_id}: {e}")
127
+
128
+ def load_multiple_connectors(self, connector_ids: List[str]) -> Dict[str, Type[BaseConnector]]:
129
+ """
130
+ Load multiple connectors.
131
+
132
+ Args:
133
+ connector_ids: List of connector IDs
134
+
135
+ Returns:
136
+ Dictionary of connector_id -> connector_class
137
+ """
138
+ try:
139
+ mgr = named.NamedExtensionManager(
140
+ namespace="af_connector_plugins",
141
+ names=connector_ids,
142
+ invoke_on_load=False,
143
+ )
144
+
145
+ result = {}
146
+ for ext in mgr.extensions:
147
+ connector_class = ext.obj
148
+ if not issubclass(connector_class, BaseConnector):
149
+ self.logger.warning(
150
+ f"Skipping {ext.name}: not a BaseConnector subclass"
151
+ )
152
+ continue
153
+
154
+ result[ext.name] = connector_class
155
+ self._loaded_connectors[ext.name] = connector_class
156
+
157
+ return result
158
+ except Exception as e:
159
+ self.logger.error(f"Failed to load connectors {connector_ids}: {e}")
160
+ raise ConnectorError(f"Failed to load connectors: {e}")
161
+
162
+ def validate_connector(self, connector_id: str) -> bool:
163
+ """
164
+ Validate a connector.
165
+
166
+ Args:
167
+ connector_id: The connector ID
168
+
169
+ Returns:
170
+ True if connector is valid, False otherwise
171
+ """
172
+ try:
173
+ connector_class = self.load_connector(connector_id)
174
+
175
+ # Check required attributes
176
+ if hasattr(connector_class, "TOOL_ID"):
177
+ if not connector_class.TOOL_ID:
178
+ self.logger.error(f"Connector {connector_id} has empty TOOL_ID")
179
+ return False
180
+ elif hasattr(connector_class, "AGENT_ID"):
181
+ if not connector_class.AGENT_ID:
182
+ self.logger.error(f"Connector {connector_id} has empty AGENT_ID")
183
+ return False
184
+ else:
185
+ self.logger.error(f"Connector {connector_id} missing TOOL_ID or AGENT_ID")
186
+ return False
187
+
188
+ # Check required methods
189
+ if not hasattr(connector_class, "invoke"):
190
+ self.logger.error(f"Connector {connector_id} missing invoke method")
191
+ return False
192
+
193
+ return True
194
+ except Exception as e:
195
+ self.logger.error(f"Connector {connector_id} validation failed: {e}")
196
+ return False
197
+
198
+ def get_connector_schema(self, connector_id: str) -> Dict[str, Any]:
199
+ """
200
+ Get the schema for a connector.
201
+
202
+ Args:
203
+ connector_id: The connector ID
204
+
205
+ Returns:
206
+ Connector schema
207
+ """
208
+ try:
209
+ connector_class = self.load_connector(connector_id)
210
+
211
+ # Create a dummy context for schema generation
212
+ import logging
213
+ import httpx
214
+ from ..auth.token_cache import TokenManager, VaultClient
215
+
216
+ dummy_http = httpx.AsyncClient()
217
+ dummy_vault = VaultClient("http://localhost", dummy_http, logging.getLogger())
218
+ dummy_token_manager = TokenManager("default", dummy_vault)
219
+
220
+ dummy_context = ConnectorContext(
221
+ tenant_id="default",
222
+ user_id="system",
223
+ http=dummy_http,
224
+ token_manager=dummy_token_manager,
225
+ logger=logging.getLogger(),
226
+ )
227
+
228
+ # Create instance to get schema
229
+ instance = connector_class(dummy_context)
230
+
231
+ if hasattr(instance, "get_schema"):
232
+ return instance.get_schema()
233
+ else:
234
+ return {
235
+ "connector_id": connector_id,
236
+ "methods": [],
237
+ "description": "Schema not available",
238
+ }
239
+ except Exception as e:
240
+ self.logger.error(f"Failed to get schema for {connector_id}: {e}")
241
+ return {
242
+ "connector_id": connector_id,
243
+ "error": str(e),
244
+ }
245
+
246
+ def clear_cache(self):
247
+ """Clear the connector cache."""
248
+ self._loaded_connectors.clear()
249
+ self.logger.info("Connector cache cleared")
250
+
251
+ def get_loaded_connectors(self) -> Dict[str, Type[BaseConnector]]:
252
+ """Get all loaded connectors."""
253
+ return self._loaded_connectors.copy()
254
+
255
+
256
+ # Global registry instance
257
+ _registry = ConnectorRegistry()
258
+
259
+
260
+ def get_connector_registry() -> ConnectorRegistry:
261
+ """Get the global connector registry."""
262
+ return _registry
af_sdk/dx/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from .decorators import tool
2
+ from .runtime import ToolFabric, AgentFabric, MCPServer, Agent
3
+
4
+ __all__ = [
5
+ "tool",
6
+ "ToolFabric",
7
+ "AgentFabric",
8
+ "MCPServer",
9
+ "Agent",
10
+ ]
11
+
12
+
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+ from functools import wraps
5
+
6
+
7
+ class ToolFunction:
8
+ """Simple wrapper that carries metadata for a Python function tool."""
9
+
10
+ def __init__(self, fn: Callable[..., Any], name: Optional[str] = None, description: Optional[str] = None):
11
+ self.fn = fn
12
+ self.name = name or fn.__name__
13
+ self.description = description or (fn.__doc__ or "")
14
+
15
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
16
+ return self.fn(*args, **kwargs)
17
+
18
+ def spec(self) -> Dict[str, Any]:
19
+ return {"name": self.name, "description": self.description}
20
+
21
+
22
+ def tool(fn: Callable[..., Any] | None = None, *, name: Optional[str] = None, description: Optional[str] = None):
23
+ """Decorator to mark a function as a tool.
24
+
25
+ Usage:
26
+ @tool
27
+ def do(x: str) -> str: ...
28
+ """
29
+
30
+ def _wrap(func: Callable[..., Any]) -> ToolFunction:
31
+ wrapped = ToolFunction(func, name=name, description=description)
32
+ @wraps(func)
33
+ def inner(*args: Any, **kwargs: Any) -> Any:
34
+ return wrapped(*args, **kwargs)
35
+ inner._af_tool = wrapped # type: ignore[attr-defined]
36
+ return inner # type: ignore[return-value]
37
+
38
+ return _wrap if fn is None else _wrap(fn)
39
+
40
+