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.

af_sdk/dx/runtime.py ADDED
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ import os
5
+ import httpx
6
+
7
+
8
+ def _base_headers(token: str, tenant_id: Optional[str]) -> Dict[str, str]:
9
+ headers = {"Authorization": f"Bearer {token}"}
10
+ if tenant_id:
11
+ headers["X-Tenant-Id"] = tenant_id
12
+ # Dev helper: allow overriding scopes from env for local testing
13
+ debug_scopes = os.getenv("FABRIQ_DEBUG_SCOPES")
14
+ if debug_scopes is not None:
15
+ headers["X-Debug-Scopes"] = debug_scopes
16
+ return headers
17
+
18
+
19
+ class ToolFabric:
20
+ """Thin facade over Fabriq provider proxy endpoints (e.g., Slack).
21
+
22
+ This class lets developers think in terms of a "fabric" of tools provided
23
+ by a vendor, while under the hood we call the Gateway proxy endpoints.
24
+ """
25
+
26
+ def __init__(self, *, provider: str, base_url: str, access_token: str, tenant_id: Optional[str] = None):
27
+ self.provider = provider
28
+ self.base_url = base_url.rstrip("/")
29
+ self.token = access_token
30
+ self.tenant_id = tenant_id
31
+
32
+ def get_tools(self, names: List[str]) -> List[str]:
33
+ # Placeholder: simply returns opaque method identifiers as strings the Agent understands
34
+ return [f"{self.provider}:{name}" for name in names]
35
+
36
+ def invoke(self, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
37
+ url = f"{self.base_url}/api/v1/proxy/{self.provider}/{action}"
38
+ with httpx.Client(timeout=30.0) as c:
39
+ r = c.post(url, json=params, headers=_base_headers(self.token, self.tenant_id))
40
+ r.raise_for_status()
41
+ return r.json()
42
+
43
+
44
+ class MCPServer:
45
+ """Facade for an MCP server registered in Fabriq (proxy layer)."""
46
+
47
+ def __init__(self, *, server_id: str, base_url: str, access_token: str, tenant_id: Optional[str] = None):
48
+ self.server_id = server_id
49
+ self.base_url = base_url.rstrip("/")
50
+ self.token = access_token
51
+ self.tenant_id = tenant_id
52
+
53
+ def get_tools(self, names: List[str]) -> List[str]:
54
+ return [f"mcp:{self.server_id}:{name}" for name in names]
55
+
56
+ def invoke(self, tool: str, args: Dict[str, Any]) -> Dict[str, Any]:
57
+ url = f"{self.base_url}/api/v1/proxy/mcp/{self.server_id}/invoke"
58
+ payload = {"payload": {"tool": tool, "args": args}}
59
+ with httpx.Client(timeout=60.0) as c:
60
+ r = c.post(url, json=payload, headers=_base_headers(self.token, self.tenant_id))
61
+ r.raise_for_status()
62
+ return r.json()
63
+
64
+
65
+ class AgentFabric:
66
+ """Future-facing A2A discovery placeholder.
67
+
68
+ Today it simply stores identifiers of other agents by id.
69
+ """
70
+
71
+ def __init__(self, *, base_url: str, access_token: str, tenant_id: Optional[str] = None):
72
+ self.base_url = base_url.rstrip("/")
73
+ self.token = access_token
74
+ self.tenant_id = tenant_id
75
+
76
+ def get_agents(self, ids: List[str]) -> List[str]:
77
+ return ids
78
+
79
+ def invoke_agent(self, agent_id: str, input: Dict[str, Any]) -> Dict[str, Any]:
80
+ url = f"{self.base_url}/api/v1/agents/{agent_id}/invoke"
81
+ with httpx.Client(timeout=60.0) as c:
82
+ r = c.post(url, json={"input": input, "context": {}}, headers=_base_headers(self.token, self.tenant_id))
83
+ r.raise_for_status()
84
+ return r.json()
85
+
86
+
87
+ class Agent:
88
+ """Minimal orchestrator that can call tools and agents.
89
+
90
+ tools: a mixed list of
91
+ - wrapped local functions decorated with @tool (call directly), or
92
+ - string references produced by ToolFabric/MCPServer (we route accordingly).
93
+ agents: a list of agent ids discoverable via AgentFabric.get_agents().
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ *,
99
+ system_prompt: str,
100
+ tools: List[Any],
101
+ agents: List[str],
102
+ base_url: str,
103
+ access_token: str,
104
+ tenant_id: Optional[str] = None,
105
+ provider_fabrics: Optional[Dict[str, ToolFabric]] = None,
106
+ mcp_servers: Optional[Dict[str, MCPServer]] = None,
107
+ agent_fabric: Optional[AgentFabric] = None,
108
+ ) -> None:
109
+ self.system_prompt = system_prompt
110
+ self.tools = tools
111
+ self.agents = agents
112
+ self.base_url = base_url.rstrip("/")
113
+ self.token = access_token
114
+ self.tenant_id = tenant_id
115
+ self.provider_fabrics = provider_fabrics or {}
116
+ self.mcp_servers = mcp_servers or {}
117
+ self.agent_fabric = agent_fabric or AgentFabric(base_url=base_url, access_token=access_token, tenant_id=tenant_id)
118
+
119
+ def _is_wrapped_tool(self, obj: Any) -> bool:
120
+ return hasattr(obj, "_af_tool")
121
+
122
+ def run(self, instruction: str) -> str:
123
+ # Super-minimal router: if instruction mentions "slack" and "summary",
124
+ # fetch recent messages via a slack ToolFabric and send to a summarizer agent.
125
+ inst = instruction.lower()
126
+ if "slack" in inst and "summary" in inst:
127
+ slack: Optional[ToolFabric] = self.provider_fabrics.get("slack")
128
+ if not slack:
129
+ return "No slack ToolFabric configured."
130
+ # Find a channel id (prefer env, else pick first from channels.list)
131
+ channel_id = os.getenv("SLACK_DEFAULT_CHANNEL")
132
+ if not channel_id:
133
+ try:
134
+ ch_resp = slack.invoke("channels.list", {})
135
+ channels = (ch_resp.get("channels") or ch_resp.get("items") or [])
136
+ if channels:
137
+ channel_id = channels[0].get("id")
138
+ except Exception:
139
+ channel_id = None
140
+ if not channel_id:
141
+ return "Could not determine a Slack channel id; set SLACK_DEFAULT_CHANNEL or ensure channels.list works."
142
+
143
+ # Fetch messages
144
+ hist = slack.invoke("conversations.history", {"channel": channel_id, "limit": 50})
145
+ # Gateway proxy normalizes to { items: [...] }, but also returns provider-shape under raw
146
+ messages = hist.get("items") or hist.get("messages") or []
147
+ if not messages:
148
+ return "No Slack messages available or Slack is not connected. Connect Slack in the UI (Tools tab) for the active tenant or set SLACK_DEFAULT_CHANNEL."
149
+ lines = [m.get("text", "").strip() for m in messages if m.get("text")]
150
+ corpus = "\n".join(lines)
151
+ # Call first collaborator agent named 'summarizer' if present
152
+ target = next((a for a in self.agents if a == "summarizer"), None)
153
+ if not target:
154
+ return "No summarizer agent configured."
155
+ result = self.agent_fabric.invoke_agent(target, {"text": corpus})
156
+ # result shape: { output, metadata, logs, status }
157
+ return result.get("output") or str(result)
158
+
159
+ # Fallback: try local tools if any
160
+ for t in self.tools:
161
+ if self._is_wrapped_tool(t):
162
+ try:
163
+ out = t(instruction) # type: ignore[misc]
164
+ if isinstance(out, str) and out:
165
+ return out
166
+ except Exception:
167
+ pass
168
+ return "No handler matched the instruction."
169
+
170
+