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.
- signalops-0.1.0/PKG-INFO +20 -0
- signalops-0.1.0/setup.cfg +4 -0
- signalops-0.1.0/setup.py +22 -0
- signalops-0.1.0/signal_sdk/__init__.py +4 -0
- signalops-0.1.0/signal_sdk/client.py +150 -0
- signalops-0.1.0/signalops.egg-info/PKG-INFO +20 -0
- signalops-0.1.0/signalops.egg-info/SOURCES.txt +8 -0
- signalops-0.1.0/signalops.egg-info/dependency_links.txt +1 -0
- signalops-0.1.0/signalops.egg-info/requires.txt +2 -0
- signalops-0.1.0/signalops.egg-info/top_level.txt +1 -0
signalops-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
signalops-0.1.0/setup.py
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
signal_sdk
|