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.
Files changed (90) hide show
  1. hyperforge/__init__.py +16 -0
  2. hyperforge/agent.py +81 -0
  3. hyperforge/api/__init__.py +20 -0
  4. hyperforge/api/app.py +155 -0
  5. hyperforge/api/authentication.py +271 -0
  6. hyperforge/api/commands.py +33 -0
  7. hyperforge/api/internal/__init__.py +4 -0
  8. hyperforge/api/internal/inspect.py +30 -0
  9. hyperforge/api/internal/router.py +3 -0
  10. hyperforge/api/logging.py +18 -0
  11. hyperforge/api/models.py +129 -0
  12. hyperforge/api/session.py +197 -0
  13. hyperforge/api/settings.py +38 -0
  14. hyperforge/api/utils.py +354 -0
  15. hyperforge/api/v1/__init__.py +23 -0
  16. hyperforge/api/v1/agents.py +531 -0
  17. hyperforge/api/v1/interaction.py +430 -0
  18. hyperforge/api/v1/mcp_content.py +311 -0
  19. hyperforge/api/v1/mcp_interaction.py +322 -0
  20. hyperforge/api/v1/oauth.py +60 -0
  21. hyperforge/api/v1/prompt.py +129 -0
  22. hyperforge/api/v1/router.py +3 -0
  23. hyperforge/api/v1/schema.py +56 -0
  24. hyperforge/api/v1/session.py +182 -0
  25. hyperforge/api/v1/utils.py +12 -0
  26. hyperforge/api/v1/workflows.py +643 -0
  27. hyperforge/arag.py +28 -0
  28. hyperforge/broker/__init__.py +52 -0
  29. hyperforge/broker/local.py +116 -0
  30. hyperforge/broker/redis.py +161 -0
  31. hyperforge/configure.py +571 -0
  32. hyperforge/context/__init__.py +0 -0
  33. hyperforge/context/agent.py +377 -0
  34. hyperforge/context/config.py +103 -0
  35. hyperforge/database.py +3 -0
  36. hyperforge/db/__init__.py +6 -0
  37. hyperforge/db/agents.py +1521 -0
  38. hyperforge/db/encryption.py +91 -0
  39. hyperforge/db/exceptions.py +26 -0
  40. hyperforge/db/settings.py +16 -0
  41. hyperforge/db/workflow_cleanup.py +69 -0
  42. hyperforge/definition.py +13 -0
  43. hyperforge/driver.py +31 -0
  44. hyperforge/dummy.py +28 -0
  45. hyperforge/engine.py +189 -0
  46. hyperforge/exceptions.py +14 -0
  47. hyperforge/feature_flag.py +105 -0
  48. hyperforge/fixtures.py +602 -0
  49. hyperforge/interaction.py +116 -0
  50. hyperforge/llm.py +75 -0
  51. hyperforge/manager.py +432 -0
  52. hyperforge/memory/__init__.py +5 -0
  53. hyperforge/memory/memory.py +974 -0
  54. hyperforge/minimal_fixtures.py +75 -0
  55. hyperforge/models.py +336 -0
  56. hyperforge/nua.py +336 -0
  57. hyperforge/openapi.py +63 -0
  58. hyperforge/prompts.py +188 -0
  59. hyperforge/pubsub.py +90 -0
  60. hyperforge/py.typed +0 -0
  61. hyperforge/redis_utils.py +82 -0
  62. hyperforge/retrieval/__init__.py +0 -0
  63. hyperforge/retrieval/agent.py +169 -0
  64. hyperforge/retrieval/config.py +94 -0
  65. hyperforge/server/__init__.py +5 -0
  66. hyperforge/server/cache.py +131 -0
  67. hyperforge/server/run.py +109 -0
  68. hyperforge/server/sandbox.py +60 -0
  69. hyperforge/server/session.py +421 -0
  70. hyperforge/server/settings.py +47 -0
  71. hyperforge/server/utils.py +57 -0
  72. hyperforge/server/web.py +31 -0
  73. hyperforge/settings.py +18 -0
  74. hyperforge/standalone/__init__.py +5 -0
  75. hyperforge/standalone/agent.py +189 -0
  76. hyperforge/standalone/app.py +264 -0
  77. hyperforge/standalone/config.py +137 -0
  78. hyperforge/standalone/const.py +1 -0
  79. hyperforge/standalone/run.py +60 -0
  80. hyperforge/standalone/settings.py +133 -0
  81. hyperforge/standalone/ui_router.py +241 -0
  82. hyperforge/trace.py +42 -0
  83. hyperforge/utils/__init__.py +112 -0
  84. hyperforge/utils/http.py +48 -0
  85. hyperforge/workflows.py +44 -0
  86. hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
  87. hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
  88. hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
  89. hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
  90. 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]