hyperforge 1.0.0.post19__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.
- hyperforge/__init__.py +16 -0
- hyperforge/agent.py +81 -0
- hyperforge/api/__init__.py +20 -0
- hyperforge/api/app.py +155 -0
- hyperforge/api/authentication.py +271 -0
- hyperforge/api/commands.py +33 -0
- hyperforge/api/internal/__init__.py +4 -0
- hyperforge/api/internal/inspect.py +30 -0
- hyperforge/api/internal/router.py +3 -0
- hyperforge/api/logging.py +18 -0
- hyperforge/api/models.py +129 -0
- hyperforge/api/session.py +197 -0
- hyperforge/api/settings.py +38 -0
- hyperforge/api/utils.py +354 -0
- hyperforge/api/v1/__init__.py +23 -0
- hyperforge/api/v1/agents.py +531 -0
- hyperforge/api/v1/interaction.py +430 -0
- hyperforge/api/v1/mcp_content.py +311 -0
- hyperforge/api/v1/mcp_interaction.py +322 -0
- hyperforge/api/v1/oauth.py +60 -0
- hyperforge/api/v1/prompt.py +129 -0
- hyperforge/api/v1/router.py +3 -0
- hyperforge/api/v1/schema.py +56 -0
- hyperforge/api/v1/session.py +182 -0
- hyperforge/api/v1/utils.py +12 -0
- hyperforge/api/v1/workflows.py +643 -0
- hyperforge/arag.py +28 -0
- hyperforge/broker/__init__.py +52 -0
- hyperforge/broker/local.py +116 -0
- hyperforge/broker/redis.py +161 -0
- hyperforge/configure.py +571 -0
- hyperforge/context/__init__.py +0 -0
- hyperforge/context/agent.py +377 -0
- hyperforge/context/config.py +103 -0
- hyperforge/database.py +3 -0
- hyperforge/db/__init__.py +6 -0
- hyperforge/db/agents.py +1521 -0
- hyperforge/db/encryption.py +91 -0
- hyperforge/db/exceptions.py +26 -0
- hyperforge/db/settings.py +16 -0
- hyperforge/db/workflow_cleanup.py +69 -0
- hyperforge/definition.py +13 -0
- hyperforge/driver.py +31 -0
- hyperforge/dummy.py +28 -0
- hyperforge/engine.py +189 -0
- hyperforge/exceptions.py +14 -0
- hyperforge/feature_flag.py +105 -0
- hyperforge/fixtures.py +602 -0
- hyperforge/interaction.py +116 -0
- hyperforge/llm.py +75 -0
- hyperforge/manager.py +432 -0
- hyperforge/memory/__init__.py +5 -0
- hyperforge/memory/memory.py +974 -0
- hyperforge/minimal_fixtures.py +75 -0
- hyperforge/models.py +336 -0
- hyperforge/nua.py +336 -0
- hyperforge/openapi.py +63 -0
- hyperforge/prompts.py +188 -0
- hyperforge/pubsub.py +90 -0
- hyperforge/py.typed +0 -0
- hyperforge/redis_utils.py +82 -0
- hyperforge/retrieval/__init__.py +0 -0
- hyperforge/retrieval/agent.py +169 -0
- hyperforge/retrieval/config.py +94 -0
- hyperforge/server/__init__.py +5 -0
- hyperforge/server/cache.py +131 -0
- hyperforge/server/run.py +109 -0
- hyperforge/server/sandbox.py +60 -0
- hyperforge/server/session.py +421 -0
- hyperforge/server/settings.py +47 -0
- hyperforge/server/utils.py +57 -0
- hyperforge/server/web.py +31 -0
- hyperforge/settings.py +18 -0
- hyperforge/standalone/__init__.py +5 -0
- hyperforge/standalone/agent.py +189 -0
- hyperforge/standalone/app.py +264 -0
- hyperforge/standalone/config.py +137 -0
- hyperforge/standalone/const.py +1 -0
- hyperforge/standalone/run.py +60 -0
- hyperforge/standalone/settings.py +133 -0
- hyperforge/standalone/ui_router.py +241 -0
- hyperforge/trace.py +42 -0
- hyperforge/utils/__init__.py +112 -0
- hyperforge/utils/http.py +48 -0
- hyperforge/workflows.py +44 -0
- hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
- hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
- hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
- hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
- hyperforge-1.0.0.post19.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import AsyncIterator
|
|
3
|
+
|
|
4
|
+
from hyperforge.pubsub import AgentMessage, StartInteraction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentTimeoutError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Broker(ABC):
|
|
12
|
+
# Activation
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def publish_activation(
|
|
15
|
+
self, msg: StartInteraction, trace: dict[str, str]
|
|
16
|
+
) -> None: ...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def subscribe_activations(
|
|
20
|
+
self,
|
|
21
|
+
) -> AsyncIterator[tuple[StartInteraction, dict[str, str]]]:
|
|
22
|
+
"""Yields (StartInteraction, trace_headers) pairs. Called only by the server."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
# Answer stream
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def publish(self, topic: str, message: AgentMessage) -> None: ...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def subscribe(
|
|
31
|
+
self, topic: str, from_cursor: str = "0"
|
|
32
|
+
) -> AsyncIterator[tuple[str, AgentMessage]]:
|
|
33
|
+
"""Yields (cursor, message) pairs. Raises AgentTimeoutError on keepalive timeout."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
# Reply channel — used for feedback and OAuth callbacks
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def send_reply(self, key: str, payload: str) -> None: ...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def receive_reply(self, key: str, timeout_ms: int) -> str | None: ...
|
|
42
|
+
|
|
43
|
+
# Lifecycle
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def keepalive_seconds(self) -> float: ...
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def initialize(self) -> None: ...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def finalize(self) -> None: ...
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import AsyncIterator
|
|
3
|
+
|
|
4
|
+
from lru import LRU
|
|
5
|
+
from pydantic import TypeAdapter
|
|
6
|
+
|
|
7
|
+
from hyperforge.broker import AgentTimeoutError, Broker
|
|
8
|
+
from hyperforge.pubsub import AgentMessage, StartInteraction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LocalBroker(Broker):
|
|
12
|
+
"""In-process broker implementation. No Redis required.
|
|
13
|
+
API and server must share the same instance."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, keepalive_ms: int = 20000, max_streams: int = 500):
|
|
16
|
+
self._keepalive_ms = keepalive_ms
|
|
17
|
+
self._activation_queue: asyncio.Queue[
|
|
18
|
+
tuple[StartInteraction, dict[str, str]]
|
|
19
|
+
] = asyncio.Queue()
|
|
20
|
+
# topic -> (messages list, condition). Bounded by LRU to avoid unbounded growth.
|
|
21
|
+
self._streams: LRU = LRU(max_streams)
|
|
22
|
+
# key -> queue (single item). Deleted after receive_reply.
|
|
23
|
+
self._reply_channels: dict[str, asyncio.Queue[str]] = {}
|
|
24
|
+
self._counter = 0
|
|
25
|
+
|
|
26
|
+
def _next_id(self) -> str:
|
|
27
|
+
self._counter += 1
|
|
28
|
+
return str(self._counter)
|
|
29
|
+
|
|
30
|
+
def _get_or_create_stream(
|
|
31
|
+
self, topic: str
|
|
32
|
+
) -> tuple[list[tuple[str, str]], asyncio.Condition]:
|
|
33
|
+
if topic not in self._streams:
|
|
34
|
+
self._streams[topic] = ([], asyncio.Condition())
|
|
35
|
+
return self._streams[topic]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def keepalive_seconds(self) -> float:
|
|
39
|
+
return self._keepalive_ms / 1000
|
|
40
|
+
|
|
41
|
+
async def publish_activation(
|
|
42
|
+
self, msg: StartInteraction, trace: dict[str, str]
|
|
43
|
+
) -> None:
|
|
44
|
+
await self._activation_queue.put((msg, trace))
|
|
45
|
+
|
|
46
|
+
async def subscribe_activations(
|
|
47
|
+
self,
|
|
48
|
+
) -> AsyncIterator[tuple[StartInteraction, dict[str, str]]]:
|
|
49
|
+
while True:
|
|
50
|
+
msg, trace = await self._activation_queue.get()
|
|
51
|
+
yield msg, trace
|
|
52
|
+
|
|
53
|
+
async def publish(self, topic: str, message: AgentMessage) -> None:
|
|
54
|
+
messages, condition = self._get_or_create_stream(topic)
|
|
55
|
+
cursor = self._next_id()
|
|
56
|
+
async with condition:
|
|
57
|
+
messages.append((cursor, message.model_dump_json()))
|
|
58
|
+
condition.notify_all()
|
|
59
|
+
|
|
60
|
+
async def subscribe(
|
|
61
|
+
self, topic: str, from_cursor: str = "0"
|
|
62
|
+
) -> AsyncIterator[tuple[str, AgentMessage]]:
|
|
63
|
+
messages, condition = self._get_or_create_stream(topic)
|
|
64
|
+
adapter: TypeAdapter[AgentMessage] = TypeAdapter(AgentMessage)
|
|
65
|
+
|
|
66
|
+
# Find the starting index from the cursor
|
|
67
|
+
last_index = 0
|
|
68
|
+
if from_cursor != "0":
|
|
69
|
+
for i, (cursor, _) in enumerate(messages):
|
|
70
|
+
if cursor == from_cursor:
|
|
71
|
+
last_index = i + 1
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
while True:
|
|
75
|
+
async with condition:
|
|
76
|
+
if last_index < len(messages):
|
|
77
|
+
batch = messages[last_index:]
|
|
78
|
+
last_index = len(messages)
|
|
79
|
+
else:
|
|
80
|
+
try:
|
|
81
|
+
await asyncio.wait_for(
|
|
82
|
+
condition.wait(),
|
|
83
|
+
timeout=self._keepalive_ms / 1000,
|
|
84
|
+
)
|
|
85
|
+
except asyncio.TimeoutError:
|
|
86
|
+
raise AgentTimeoutError(topic)
|
|
87
|
+
batch = messages[last_index:]
|
|
88
|
+
last_index = len(messages)
|
|
89
|
+
|
|
90
|
+
for cursor, raw in batch:
|
|
91
|
+
yield cursor, adapter.validate_json(raw)
|
|
92
|
+
|
|
93
|
+
async def send_reply(self, key: str, payload: str) -> None:
|
|
94
|
+
if key not in self._reply_channels:
|
|
95
|
+
self._reply_channels[key] = asyncio.Queue(maxsize=1)
|
|
96
|
+
await self._reply_channels[key].put(payload)
|
|
97
|
+
|
|
98
|
+
async def receive_reply(self, key: str, timeout_ms: int) -> str | None:
|
|
99
|
+
if key not in self._reply_channels:
|
|
100
|
+
self._reply_channels[key] = asyncio.Queue(maxsize=1)
|
|
101
|
+
try:
|
|
102
|
+
return await asyncio.wait_for(
|
|
103
|
+
self._reply_channels[key].get(),
|
|
104
|
+
timeout=timeout_ms / 1000,
|
|
105
|
+
)
|
|
106
|
+
except asyncio.TimeoutError:
|
|
107
|
+
return None
|
|
108
|
+
finally:
|
|
109
|
+
self._reply_channels.pop(key, None)
|
|
110
|
+
|
|
111
|
+
async def initialize(self) -> None:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
async def finalize(self) -> None:
|
|
115
|
+
self._streams.clear()
|
|
116
|
+
self._reply_channels.clear()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from typing import AsyncIterator, cast
|
|
4
|
+
|
|
5
|
+
import opentelemetry.propagate
|
|
6
|
+
from pydantic import TypeAdapter
|
|
7
|
+
from redis.asyncio import Redis, ResponseError
|
|
8
|
+
|
|
9
|
+
from hyperforge import logger
|
|
10
|
+
from hyperforge.broker import AgentTimeoutError, Broker
|
|
11
|
+
from hyperforge.pubsub import AgentMessage, StartInteraction
|
|
12
|
+
from hyperforge.redis_utils import ManualStreamKeysRedisCluster
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisBroker(Broker):
|
|
16
|
+
def __init__(self, client: Redis, activate_subject: str, keepalive_ms: int):
|
|
17
|
+
self._client = client
|
|
18
|
+
self._activate_subject = activate_subject
|
|
19
|
+
self._keepalive_ms = int(keepalive_ms)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def keepalive_seconds(self) -> float:
|
|
23
|
+
return self._keepalive_ms / 1000
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_url(
|
|
27
|
+
cls,
|
|
28
|
+
url: str,
|
|
29
|
+
activate_subject: str,
|
|
30
|
+
keepalive_ms: int,
|
|
31
|
+
cluster_mode: bool = False,
|
|
32
|
+
) -> "RedisBroker":
|
|
33
|
+
if cluster_mode:
|
|
34
|
+
client = cast(
|
|
35
|
+
Redis,
|
|
36
|
+
ManualStreamKeysRedisCluster.from_url(
|
|
37
|
+
url=url,
|
|
38
|
+
decode_responses=True,
|
|
39
|
+
dynamic_startup_nodes=False,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
client = Redis.from_url(
|
|
44
|
+
url,
|
|
45
|
+
decode_responses=True,
|
|
46
|
+
)
|
|
47
|
+
return cls(client, activate_subject, keepalive_ms)
|
|
48
|
+
|
|
49
|
+
async def publish_activation(
|
|
50
|
+
self, msg: StartInteraction, trace: dict[str, str]
|
|
51
|
+
) -> None:
|
|
52
|
+
await self._client.xadd(
|
|
53
|
+
self._activate_subject,
|
|
54
|
+
{"msg": msg.model_dump_json(), "trace": json.dumps(trace)},
|
|
55
|
+
maxlen=100,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def subscribe_activations(
|
|
59
|
+
self,
|
|
60
|
+
) -> AsyncIterator[tuple[StartInteraction, dict[str, str]]]:
|
|
61
|
+
try:
|
|
62
|
+
await self._client.xgroup_create(
|
|
63
|
+
name=self._activate_subject,
|
|
64
|
+
groupname="arag_server",
|
|
65
|
+
mkstream=True,
|
|
66
|
+
)
|
|
67
|
+
except ResponseError as e:
|
|
68
|
+
if "BUSYGROUP" not in str(e):
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
try:
|
|
73
|
+
response = await self._client.xreadgroup(
|
|
74
|
+
groupname="arag_server",
|
|
75
|
+
consumername="any_server",
|
|
76
|
+
streams={self._activate_subject: ">"},
|
|
77
|
+
block=1000,
|
|
78
|
+
count=1,
|
|
79
|
+
noack=True,
|
|
80
|
+
)
|
|
81
|
+
if not response:
|
|
82
|
+
continue
|
|
83
|
+
[_stream, messages] = response[0]
|
|
84
|
+
if messages:
|
|
85
|
+
[_msgid, fields] = messages[0]
|
|
86
|
+
msg = StartInteraction.model_validate_json(fields["msg"])
|
|
87
|
+
trace = json.loads(fields.get("trace", "{}"))
|
|
88
|
+
yield msg, trace
|
|
89
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
90
|
+
logger.info("Activation subscription cancelled, exiting...")
|
|
91
|
+
break
|
|
92
|
+
except Exception:
|
|
93
|
+
logger.exception("Error while subscribing to activations, retrying...")
|
|
94
|
+
await asyncio.sleep(1)
|
|
95
|
+
|
|
96
|
+
async def publish(self, topic: str, message: AgentMessage) -> None:
|
|
97
|
+
async with self._client.pipeline() as pipe:
|
|
98
|
+
await (
|
|
99
|
+
pipe.xadd(topic, {"msg": message.model_dump_json()}, maxlen=100)
|
|
100
|
+
.expire(topic, 300)
|
|
101
|
+
.execute()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def subscribe(
|
|
105
|
+
self, topic: str, from_cursor: str = "0"
|
|
106
|
+
) -> AsyncIterator[tuple[str, AgentMessage]]:
|
|
107
|
+
cursor = from_cursor
|
|
108
|
+
retries = 0
|
|
109
|
+
while True:
|
|
110
|
+
try:
|
|
111
|
+
response = await self._client.xread(
|
|
112
|
+
{topic: cursor},
|
|
113
|
+
block=max(50, int(self._keepalive_ms)),
|
|
114
|
+
)
|
|
115
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
116
|
+
logger.info(f"Subscription to topic '{topic}' cancelled, exiting...")
|
|
117
|
+
break
|
|
118
|
+
except Exception:
|
|
119
|
+
logger.exception(
|
|
120
|
+
f"Error while subscribing to topic '{topic}', retrying..."
|
|
121
|
+
)
|
|
122
|
+
retries += 1
|
|
123
|
+
if retries > 5:
|
|
124
|
+
logger.error(
|
|
125
|
+
f"Too many errors while subscribing to topic '{topic}', giving up..."
|
|
126
|
+
)
|
|
127
|
+
raise AgentTimeoutError(topic)
|
|
128
|
+
await asyncio.sleep(1)
|
|
129
|
+
continue
|
|
130
|
+
if not response:
|
|
131
|
+
raise AgentTimeoutError(topic)
|
|
132
|
+
[_stream, messages] = response[0]
|
|
133
|
+
for msgid, fields in messages:
|
|
134
|
+
cursor = msgid
|
|
135
|
+
obj: AgentMessage = TypeAdapter(AgentMessage).validate_json(
|
|
136
|
+
fields["msg"]
|
|
137
|
+
)
|
|
138
|
+
yield cursor, obj
|
|
139
|
+
|
|
140
|
+
async def send_reply(self, key: str, payload: str) -> None:
|
|
141
|
+
trace_headers: dict[str, str] = {}
|
|
142
|
+
opentelemetry.propagate.inject(trace_headers)
|
|
143
|
+
await self._client.xadd(
|
|
144
|
+
key, {"msg": payload, "trace": json.dumps(trace_headers)}, maxlen=100
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def receive_reply(self, key: str, timeout_ms: int) -> str | None:
|
|
148
|
+
response = await self._client.xread(
|
|
149
|
+
{key: "$"},
|
|
150
|
+
block=timeout_ms,
|
|
151
|
+
count=1,
|
|
152
|
+
)
|
|
153
|
+
if not response:
|
|
154
|
+
return None
|
|
155
|
+
return response[0][1][0][1]["msg"]
|
|
156
|
+
|
|
157
|
+
async def initialize(self) -> None:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
async def finalize(self) -> None:
|
|
161
|
+
await self._client.aclose() # type: ignore[attr-defined]
|