signalops 0.1.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.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: signalops
3
+ Version: 0.1.0
4
+ Summary: Operational intelligence for AI agents. Human judgment should compound, not evaporate.
5
+ Home-page: https://github.com/PranavThe/Signal-SDK
6
+ Author: Pranav Puranik
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.27.0
13
+ Requires-Dist: httpx-sse>=0.4.0
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name="signalops",
5
+ version="0.1.0",
6
+ description="Operational intelligence for AI agents. Human judgment should compound, not evaporate.",
7
+ long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
8
+ long_description_content_type="text/markdown",
9
+ author="Pranav Puranik",
10
+ url="https://github.com/PranavThe/Signal-SDK",
11
+ packages=find_packages(),
12
+ install_requires=[
13
+ "httpx>=0.27.0",
14
+ "httpx-sse>=0.4.0",
15
+ ],
16
+ python_requires=">=3.12",
17
+ classifiers=[
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ ],
22
+ )
@@ -0,0 +1,4 @@
1
+ from signal_sdk.client import CheckResult, EscalationResult, Signal
2
+
3
+ __all__ = ["CheckResult", "EscalationResult", "Signal"]
4
+
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import time
6
+ from typing import Any
7
+
8
+ import httpx
9
+ from httpx_sse import aconnect_sse
10
+
11
+
12
+ class EscalationResult:
13
+ def __init__(self, decision: str, rule_id: str | None, auto_resolved: bool = False):
14
+ self.decision = decision
15
+ self.rule_id = rule_id
16
+ self.auto_resolved = auto_resolved
17
+
18
+
19
+ class CheckResult:
20
+ def __init__(self, result: str, rule_id: str | None, reasoning: str, modification: dict | None):
21
+ self.result = result
22
+ self.rule_id = rule_id
23
+ self.reasoning = reasoning
24
+ self.modification = modification
25
+
26
+
27
+ class Signal:
28
+ def __init__(self, api_key: str, base_url: str = "http://localhost:8000"):
29
+ self.api_key = api_key
30
+ self.base_url = base_url.rstrip("/")
31
+ self.headers = {"Authorization": f"Bearer {api_key}"}
32
+
33
+ async def escalate(
34
+ self,
35
+ context: str,
36
+ question: str,
37
+ agent_id: str,
38
+ metadata: dict[str, Any] | None = None,
39
+ action: str | None = None,
40
+ timeout_seconds: int = 3600,
41
+ poll_interval_seconds: int = 3,
42
+ ) -> EscalationResult:
43
+ deadline = time.monotonic() + timeout_seconds
44
+ timeout = httpx.Timeout(30.0, read=None)
45
+ async with httpx.AsyncClient(base_url=self.base_url, headers=self.headers, timeout=timeout) as client:
46
+ response = await client.post(
47
+ "/v1/escalations",
48
+ json={
49
+ "context": context,
50
+ "question": question,
51
+ "agent_id": agent_id,
52
+ "action": action,
53
+ "metadata": metadata or {},
54
+ },
55
+ )
56
+ response.raise_for_status()
57
+ escalation_id = response.json()["escalation_id"]
58
+
59
+ try:
60
+ return await self._wait_for_stream(client, escalation_id, deadline)
61
+ except Exception:
62
+ return await self._poll_until_response(
63
+ client,
64
+ escalation_id,
65
+ deadline,
66
+ poll_interval_seconds,
67
+ )
68
+
69
+ raise TimeoutError(f"Escalation {escalation_id} did not receive a response in time")
70
+
71
+ async def _wait_for_stream(
72
+ self,
73
+ client: httpx.AsyncClient,
74
+ escalation_id: str,
75
+ deadline: float,
76
+ ) -> EscalationResult:
77
+ remaining = max(deadline - time.monotonic(), 0)
78
+ if remaining <= 0:
79
+ raise TimeoutError(f"Escalation {escalation_id} did not receive a response in time")
80
+
81
+ async with asyncio.timeout(remaining):
82
+ async with aconnect_sse(client, "GET", f"/v1/escalations/{escalation_id}/stream") as event_source:
83
+ async for sse in event_source.aiter_sse():
84
+ if not sse.data:
85
+ continue
86
+ state = json.loads(sse.data)
87
+ if state.get("event") == "created":
88
+ continue
89
+ finalized = state.get("finalized", state.get("status") == "responded")
90
+ if finalized:
91
+ return EscalationResult(
92
+ decision=state.get("human_decision"),
93
+ rule_id=state.get("rule_id"),
94
+ auto_resolved=bool(state.get("auto_resolved")),
95
+ )
96
+ if state.get("status") == "timed_out":
97
+ raise TimeoutError(f"Escalation {escalation_id} timed out")
98
+
99
+ raise TimeoutError(f"Escalation {escalation_id} did not receive a response in time")
100
+
101
+ async def _poll_until_response(
102
+ self,
103
+ client: httpx.AsyncClient,
104
+ escalation_id: str,
105
+ deadline: float,
106
+ poll_interval_seconds: int,
107
+ ) -> EscalationResult:
108
+ while time.monotonic() < deadline:
109
+ state_response = await client.get(f"/v1/escalations/{escalation_id}")
110
+ state_response.raise_for_status()
111
+ state = state_response.json()
112
+
113
+ finalized = state.get("finalized", state["status"] == "responded")
114
+ if finalized:
115
+ return EscalationResult(
116
+ decision=state["human_decision"],
117
+ rule_id=state["rule_id"],
118
+ auto_resolved=bool(state.get("auto_resolved")),
119
+ )
120
+ if state["status"] == "timed_out":
121
+ raise TimeoutError(f"Escalation {escalation_id} timed out")
122
+
123
+ await asyncio.sleep(poll_interval_seconds)
124
+
125
+ raise TimeoutError(f"Escalation {escalation_id} did not receive a response in time")
126
+
127
+ async def check(
128
+ self,
129
+ action: str,
130
+ context: dict[str, Any],
131
+ agent_id: str,
132
+ ) -> CheckResult:
133
+ async with httpx.AsyncClient(base_url=self.base_url, headers=self.headers, timeout=30.0) as client:
134
+ response = await client.post(
135
+ "/v1/check",
136
+ json={
137
+ "action": action,
138
+ "agent_id": agent_id,
139
+ "context": context,
140
+ },
141
+ )
142
+ response.raise_for_status()
143
+ data = response.json()
144
+
145
+ return CheckResult(
146
+ result=data["result"],
147
+ rule_id=data["rule_id"],
148
+ reasoning=data["reasoning"],
149
+ modification=data["modification"],
150
+ )
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: signalops
3
+ Version: 0.1.0
4
+ Summary: Operational intelligence for AI agents. Human judgment should compound, not evaporate.
5
+ Home-page: https://github.com/PranavThe/Signal-SDK
6
+ Author: Pranav Puranik
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.27.0
13
+ Requires-Dist: httpx-sse>=0.4.0
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
@@ -0,0 +1,8 @@
1
+ setup.py
2
+ signal_sdk/__init__.py
3
+ signal_sdk/client.py
4
+ signalops.egg-info/PKG-INFO
5
+ signalops.egg-info/SOURCES.txt
6
+ signalops.egg-info/dependency_links.txt
7
+ signalops.egg-info/requires.txt
8
+ signalops.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.27.0
2
+ httpx-sse>=0.4.0
@@ -0,0 +1 @@
1
+ signal_sdk