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,421 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from asyncio import Task
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from hyperforge.standalone.agent import StaticAgentManager
|
|
9
|
+
|
|
10
|
+
import nucliadb_telemetry.context
|
|
11
|
+
import nucliadb_telemetry.metrics
|
|
12
|
+
import prometheus_client
|
|
13
|
+
from aiohttp.web import Server
|
|
14
|
+
from lru import LRU
|
|
15
|
+
from nucliadb_telemetry import errors
|
|
16
|
+
from nucliadb_telemetry.utils import get_telemetry
|
|
17
|
+
from opentelemetry import trace
|
|
18
|
+
|
|
19
|
+
from hyperforge.broker import Broker
|
|
20
|
+
from hyperforge.configure import load_all_configurations, scan
|
|
21
|
+
from hyperforge.db.agents import AgentManager
|
|
22
|
+
from hyperforge.engine import State, get_state
|
|
23
|
+
from hyperforge.interaction import (
|
|
24
|
+
AnswerOperation,
|
|
25
|
+
AragAnswer,
|
|
26
|
+
ARAGException,
|
|
27
|
+
Feedback,
|
|
28
|
+
OAuthAuthenticateURL,
|
|
29
|
+
)
|
|
30
|
+
from hyperforge.memory.memory import QuestionMemory
|
|
31
|
+
from hyperforge.pubsub import (
|
|
32
|
+
AgentAnswer,
|
|
33
|
+
AgentDone,
|
|
34
|
+
AgentMessage,
|
|
35
|
+
AgentPing,
|
|
36
|
+
AgentToUserRequest,
|
|
37
|
+
OAuthRequest,
|
|
38
|
+
StartInteraction,
|
|
39
|
+
UserToAgentInteraction,
|
|
40
|
+
)
|
|
41
|
+
from hyperforge.server import SERVICE_NAME, logger
|
|
42
|
+
from hyperforge.server.cache import Cache
|
|
43
|
+
from hyperforge.server.settings import Settings
|
|
44
|
+
from hyperforge.server.utils import get_memory
|
|
45
|
+
from hyperforge.server.web import start_health_check
|
|
46
|
+
|
|
47
|
+
HOSTNAME = os.environ.get("HOSTNAME", "arag-server").encode()
|
|
48
|
+
|
|
49
|
+
answer_observer = nucliadb_telemetry.metrics.Observer("arag_answer")
|
|
50
|
+
activation_observer = nucliadb_telemetry.metrics.Observer("arag_activation")
|
|
51
|
+
answer_running = prometheus_client.Gauge(
|
|
52
|
+
"arag_running_answers_count", "Number of answering processess currently running"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tracer():
|
|
57
|
+
provider = get_telemetry(SERVICE_NAME)
|
|
58
|
+
if provider:
|
|
59
|
+
return provider.get_tracer(__name__)
|
|
60
|
+
else:
|
|
61
|
+
return trace.NoOpTracer()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SessionManager:
|
|
65
|
+
server: Optional[Server] = None
|
|
66
|
+
tasks: List[Task]
|
|
67
|
+
hooks: Optional[Dict[str, List[Callable]]] = None
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
settings: Settings,
|
|
72
|
+
broker: Broker,
|
|
73
|
+
agent_manager: Union[AgentManager, "StaticAgentManager"],
|
|
74
|
+
cache: Cache,
|
|
75
|
+
):
|
|
76
|
+
self.settings = settings
|
|
77
|
+
self.agent_manager = agent_manager
|
|
78
|
+
self.broker = broker
|
|
79
|
+
self.memory: LRU = LRU(800)
|
|
80
|
+
self.activation_task: asyncio.Task | None = None
|
|
81
|
+
self.tasks = []
|
|
82
|
+
self.cache = cache
|
|
83
|
+
|
|
84
|
+
async def activation_listener(self):
|
|
85
|
+
import opentelemetry.propagate as otel_propagate
|
|
86
|
+
from opentelemetry.context.context import Context
|
|
87
|
+
|
|
88
|
+
async for msg, trace_headers in self.broker.subscribe_activations():
|
|
89
|
+
try:
|
|
90
|
+
context = (
|
|
91
|
+
otel_propagate.extract(trace_headers)
|
|
92
|
+
if trace_headers
|
|
93
|
+
else Context()
|
|
94
|
+
)
|
|
95
|
+
with tracer().start_as_current_span("Activate agent", context):
|
|
96
|
+
await self.activate(msg)
|
|
97
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
98
|
+
logger.info("Activation listener cancelled, exiting...")
|
|
99
|
+
break
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.exception("Error processing activation message")
|
|
102
|
+
errors.capture_exception()
|
|
103
|
+
|
|
104
|
+
async def initialize(self, health_check: bool = True) -> None:
|
|
105
|
+
|
|
106
|
+
for load_module in self.settings.load_modules:
|
|
107
|
+
try:
|
|
108
|
+
scan(load_module)
|
|
109
|
+
load_all_configurations(load_module)
|
|
110
|
+
except ImportError:
|
|
111
|
+
logger.error(f"Module {load_module} could not be loaded")
|
|
112
|
+
|
|
113
|
+
self.activation_task = asyncio.create_task(self.activation_listener())
|
|
114
|
+
if health_check:
|
|
115
|
+
self.server = await start_health_check()
|
|
116
|
+
|
|
117
|
+
async def finalize(self):
|
|
118
|
+
|
|
119
|
+
for task in self.tasks:
|
|
120
|
+
if not task.done():
|
|
121
|
+
task.cancel()
|
|
122
|
+
if self.activation_task and not self.activation_task.done():
|
|
123
|
+
self.activation_task.cancel()
|
|
124
|
+
if self.server is not None:
|
|
125
|
+
await self.server.shutdown()
|
|
126
|
+
self.server = None
|
|
127
|
+
await self.broker.finalize()
|
|
128
|
+
|
|
129
|
+
await self.agent_manager.finalize()
|
|
130
|
+
|
|
131
|
+
def _remove_task(self, task: asyncio.Task):
|
|
132
|
+
if task in self.tasks:
|
|
133
|
+
self.tasks.remove(task)
|
|
134
|
+
|
|
135
|
+
async def activate(self, message: StartInteraction):
|
|
136
|
+
topic = None
|
|
137
|
+
logger.info("Activation message received: %s", message)
|
|
138
|
+
observation = activation_observer()
|
|
139
|
+
observation.start()
|
|
140
|
+
try:
|
|
141
|
+
nucliadb_telemetry.context.add_context(
|
|
142
|
+
{
|
|
143
|
+
"agent_id": message.agent_id,
|
|
144
|
+
"session_id": message.session,
|
|
145
|
+
"question_id": message.question_id,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
topic = self.question_topic(
|
|
150
|
+
message.account,
|
|
151
|
+
message.agent_id,
|
|
152
|
+
message.session,
|
|
153
|
+
message.question_id,
|
|
154
|
+
message.workflow_id,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Get or load session
|
|
158
|
+
config = await self.agent_manager.get_agent_config(
|
|
159
|
+
account=message.account,
|
|
160
|
+
agent_id=message.agent_id,
|
|
161
|
+
internal_nucliadb_url=self.settings.internal_nucliadb_url,
|
|
162
|
+
workflow_id=message.workflow_id,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
state = await get_state(
|
|
166
|
+
agent_id=message.agent_id,
|
|
167
|
+
config=config,
|
|
168
|
+
internal_nua_api=self.settings.internal_nua_api,
|
|
169
|
+
internal_nua=self.settings.internal_nua,
|
|
170
|
+
local_openai=self.settings.local_openai,
|
|
171
|
+
external_nua_api_key=self.settings.external_nua_api_key,
|
|
172
|
+
account=message.account,
|
|
173
|
+
kbid=None if self.settings.standalone else message.agent_id,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if message.session not in self.memory:
|
|
177
|
+
memory = await get_memory(
|
|
178
|
+
settings=self.settings,
|
|
179
|
+
session=message.session,
|
|
180
|
+
cache=self.cache,
|
|
181
|
+
config=config.memory,
|
|
182
|
+
agent=message.agent_id,
|
|
183
|
+
workflow_id=message.workflow_id,
|
|
184
|
+
)
|
|
185
|
+
self.memory[message.session] = memory
|
|
186
|
+
else:
|
|
187
|
+
memory = self.memory[message.session]
|
|
188
|
+
|
|
189
|
+
memory.rules = config.rules.rules
|
|
190
|
+
|
|
191
|
+
question = memory.start_question(
|
|
192
|
+
message.question,
|
|
193
|
+
question_id=message.question_id,
|
|
194
|
+
headers=message.headers,
|
|
195
|
+
arguments=message.arguments,
|
|
196
|
+
streaming=message.streaming,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
task = asyncio.create_task(
|
|
200
|
+
self.answer(
|
|
201
|
+
message.account,
|
|
202
|
+
message.agent_id,
|
|
203
|
+
message.workflow_id,
|
|
204
|
+
topic,
|
|
205
|
+
state,
|
|
206
|
+
question,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
task.add_done_callback(self._remove_task)
|
|
210
|
+
self.tasks.append(task)
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.exception("Activation exception")
|
|
214
|
+
errors.capture_exception(e)
|
|
215
|
+
observation.set_status("error")
|
|
216
|
+
if topic:
|
|
217
|
+
await self.callback(
|
|
218
|
+
topic,
|
|
219
|
+
AragAnswer(
|
|
220
|
+
exception=ARAGException(detail="Unable to start agent"),
|
|
221
|
+
operation=AnswerOperation.ERROR,
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
await self.send_message(topic, AgentDone())
|
|
225
|
+
|
|
226
|
+
observation.end()
|
|
227
|
+
|
|
228
|
+
def question_topic(
|
|
229
|
+
self,
|
|
230
|
+
account: str,
|
|
231
|
+
agent_id: str,
|
|
232
|
+
session: str,
|
|
233
|
+
question: str,
|
|
234
|
+
workflow_id: str = "default",
|
|
235
|
+
):
|
|
236
|
+
return self.settings.answers_subject.format(
|
|
237
|
+
account=account,
|
|
238
|
+
agent_id=agent_id,
|
|
239
|
+
session=session,
|
|
240
|
+
question=question,
|
|
241
|
+
workflow_id=workflow_id,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def send_message(
|
|
245
|
+
self,
|
|
246
|
+
topic: str,
|
|
247
|
+
message: AgentMessage,
|
|
248
|
+
) -> None:
|
|
249
|
+
try:
|
|
250
|
+
await self.broker.publish(topic, message)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.exception("Error publishing answer to %s", topic)
|
|
254
|
+
errors.capture_exception(e)
|
|
255
|
+
|
|
256
|
+
async def oauth(self, topic: str, oauth: OAuthAuthenticateURL):
|
|
257
|
+
await self.send_message(
|
|
258
|
+
topic,
|
|
259
|
+
OAuthRequest(oauth=oauth),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
async def get_oauth_callback(
|
|
263
|
+
self,
|
|
264
|
+
account_id: str,
|
|
265
|
+
agent_id: str,
|
|
266
|
+
session_id: str,
|
|
267
|
+
workflow_id: str,
|
|
268
|
+
question_uuid: str,
|
|
269
|
+
oauth_uuid: str,
|
|
270
|
+
timeout_ms: int = 300000,
|
|
271
|
+
) -> str | None:
|
|
272
|
+
subject = self.settings.oauth_subject.format(
|
|
273
|
+
account=account_id,
|
|
274
|
+
agent_id=agent_id,
|
|
275
|
+
session=session_id,
|
|
276
|
+
question=question_uuid,
|
|
277
|
+
oauth_uuid=oauth_uuid,
|
|
278
|
+
workflow_id=workflow_id,
|
|
279
|
+
)
|
|
280
|
+
# Cap the XREAD block time so it doesn't exceed the overall question
|
|
281
|
+
# timeout. We leave a 10 s margin so the caller can still handle the
|
|
282
|
+
# None return before the outer asyncio.timeout fires.
|
|
283
|
+
margin_ms = 10_000
|
|
284
|
+
max_block_ms = self.settings.question_timeout_seconds * 1000 - margin_ms
|
|
285
|
+
effective_timeout_ms = min(timeout_ms, max(max_block_ms, 0))
|
|
286
|
+
|
|
287
|
+
logger.info(
|
|
288
|
+
"Waiting for OAuth callback %s (timeout=%dms)",
|
|
289
|
+
oauth_uuid,
|
|
290
|
+
effective_timeout_ms,
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
payload = await self.broker.receive_reply(subject, effective_timeout_ms)
|
|
294
|
+
if payload is None:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
logger.info("OAuth callback %s received successfully", oauth_uuid)
|
|
298
|
+
return payload
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.exception("Error receiving OAuth callback %s", oauth_uuid)
|
|
301
|
+
errors.capture_exception(e)
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
async def feedback(self, topic: str, feedback: Feedback):
|
|
305
|
+
await self.send_message(
|
|
306
|
+
topic,
|
|
307
|
+
AgentToUserRequest(feedback=feedback),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
payload = await self.broker.receive_reply(
|
|
312
|
+
feedback.feedback_id, feedback.timeout_ms
|
|
313
|
+
)
|
|
314
|
+
if payload is None:
|
|
315
|
+
return None
|
|
316
|
+
return UserToAgentInteraction.model_validate_json(payload)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.exception("Error receiving feedback %s", topic)
|
|
319
|
+
errors.capture_exception(e)
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
async def callback(self, topic: str, message: AragAnswer):
|
|
323
|
+
await self.send_message(topic, AgentAnswer(answer=message))
|
|
324
|
+
|
|
325
|
+
async def keep_alive(self, topic: str):
|
|
326
|
+
while True:
|
|
327
|
+
await asyncio.sleep(self.broker.keepalive_seconds / 2)
|
|
328
|
+
await self.send_message(topic, AgentPing())
|
|
329
|
+
|
|
330
|
+
async def answer(
|
|
331
|
+
self,
|
|
332
|
+
account_id: str,
|
|
333
|
+
agent_id: str,
|
|
334
|
+
workflow_id: str,
|
|
335
|
+
topic: str,
|
|
336
|
+
state: State,
|
|
337
|
+
question_memory: QuestionMemory,
|
|
338
|
+
):
|
|
339
|
+
error = None
|
|
340
|
+
|
|
341
|
+
keepalive = asyncio.create_task(self.keep_alive(topic))
|
|
342
|
+
observation = answer_observer()
|
|
343
|
+
observation.start()
|
|
344
|
+
answer_running.inc()
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
callback = partial(self.callback, topic)
|
|
348
|
+
question_memory.set_callback_fn(callback)
|
|
349
|
+
|
|
350
|
+
feedback = partial(self.feedback, topic)
|
|
351
|
+
question_memory.set_feedback_fn(feedback)
|
|
352
|
+
|
|
353
|
+
oauth = partial(self.oauth, topic)
|
|
354
|
+
question_memory.set_oauth_fn(oauth)
|
|
355
|
+
|
|
356
|
+
oauth_callback = partial(
|
|
357
|
+
self.get_oauth_callback,
|
|
358
|
+
account_id,
|
|
359
|
+
agent_id,
|
|
360
|
+
question_memory.session.id,
|
|
361
|
+
workflow_id,
|
|
362
|
+
)
|
|
363
|
+
question_memory.set_oauth_callback_fn(oauth_callback)
|
|
364
|
+
|
|
365
|
+
await self.callback(
|
|
366
|
+
topic,
|
|
367
|
+
AragAnswer(operation=AnswerOperation.START),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async with asyncio.timeout(self.settings.question_timeout_seconds):
|
|
371
|
+
await state.agent(question_memory, state.manager)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.exception("Answering exception")
|
|
375
|
+
errors.capture_exception(e)
|
|
376
|
+
error = ARAGException(detail=str(e))
|
|
377
|
+
observation.set_status("error")
|
|
378
|
+
|
|
379
|
+
observation.end()
|
|
380
|
+
answer_running.dec()
|
|
381
|
+
keepalive.cancel()
|
|
382
|
+
|
|
383
|
+
await self.callback(
|
|
384
|
+
topic,
|
|
385
|
+
AragAnswer(
|
|
386
|
+
exception=error,
|
|
387
|
+
answer=question_memory.final_answer,
|
|
388
|
+
answer_citations=question_memory.final_answer_citations,
|
|
389
|
+
answer_urls=question_memory.final_answer_urls,
|
|
390
|
+
operation=AnswerOperation.ERROR
|
|
391
|
+
if error is not None
|
|
392
|
+
else AnswerOperation.ANSWER,
|
|
393
|
+
data_visualizations=question_memory.data_visualizations
|
|
394
|
+
if question_memory.data_visualizations
|
|
395
|
+
else None,
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
await self.send_message(
|
|
399
|
+
topic,
|
|
400
|
+
AgentDone(),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
await question_memory.save()
|
|
405
|
+
self.process_event(
|
|
406
|
+
"memory_saved",
|
|
407
|
+
{"account_id": account_id, "question_memory": question_memory},
|
|
408
|
+
)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
# Log memory errors but don't report them to the user
|
|
411
|
+
logger.exception("Error saving memory")
|
|
412
|
+
errors.capture_exception(e)
|
|
413
|
+
|
|
414
|
+
def process_event(self, event_name: str, data: dict):
|
|
415
|
+
if self.hooks is not None and event_name in self.hooks:
|
|
416
|
+
for hook in self.hooks[event_name]:
|
|
417
|
+
try:
|
|
418
|
+
hook(**data)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.exception("Error in hook for event %s", event_name)
|
|
421
|
+
errors.capture_exception(e)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
metrics_port: int = 8090
|
|
8
|
+
|
|
9
|
+
debug: bool = False
|
|
10
|
+
log_level: str = "WARNING"
|
|
11
|
+
|
|
12
|
+
session_timeout: int = 120
|
|
13
|
+
# Maximum time to answer a question
|
|
14
|
+
question_timeout_seconds: int = 300
|
|
15
|
+
|
|
16
|
+
valkey_url: str = "redis://arag-valkey-cluster"
|
|
17
|
+
valkey_cluster_mode: bool = False
|
|
18
|
+
activate_subject: str = "arag.activate"
|
|
19
|
+
answers_subject: str = (
|
|
20
|
+
"arag.{account}.{agent_id}.{workflow_id}.{session}.{question}.answer"
|
|
21
|
+
)
|
|
22
|
+
oauth_subject: str = "arag.{account}.{agent_id}.{workflow_id}.{session}.{question}.oauth.{oauth_uuid}"
|
|
23
|
+
pubsub_keepalive_seconds: float = 20
|
|
24
|
+
|
|
25
|
+
internal_nua_api: str = "http://predict.learning.svc.cluster.local:8080"
|
|
26
|
+
internal_nua: bool = False
|
|
27
|
+
local_openai: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
external_nua_api_key: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
internal_nucliadb: bool = False
|
|
32
|
+
internal_nucliadb_url: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
external_nucliadb_key: Optional[str] = None
|
|
35
|
+
external_nucliadb_url: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
sentry_url: Optional[str] = None
|
|
38
|
+
running_environment: str = "stage"
|
|
39
|
+
zone: str = "stashify"
|
|
40
|
+
load_modules: list[str] = []
|
|
41
|
+
|
|
42
|
+
# Set to True when running as a standalone server (i.e. not inside the
|
|
43
|
+
# full learning cluster). In standalone mode the agent_id is a human-
|
|
44
|
+
# readable slug from the config file, not a real KB UUID, so we resolve
|
|
45
|
+
# the kbid for internal NUA calls from the account_id request header
|
|
46
|
+
# instead.
|
|
47
|
+
standalone: bool = False
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from hyperforge.memory.memory import (
|
|
2
|
+
BaseSessionMemory,
|
|
3
|
+
EphemeralSessionMemory,
|
|
4
|
+
MemoryConfig,
|
|
5
|
+
NoMemorySessionMemory,
|
|
6
|
+
SessionMemory,
|
|
7
|
+
)
|
|
8
|
+
from hyperforge.server.cache import Cache
|
|
9
|
+
from hyperforge.server.settings import Settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_memory(
|
|
13
|
+
settings: Settings,
|
|
14
|
+
session: str,
|
|
15
|
+
cache: Cache,
|
|
16
|
+
config: MemoryConfig,
|
|
17
|
+
agent: str,
|
|
18
|
+
workflow_id: str,
|
|
19
|
+
) -> BaseSessionMemory:
|
|
20
|
+
memory: BaseSessionMemory
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
config.nucliadb is not None
|
|
24
|
+
and config.nucliadb.internal
|
|
25
|
+
and settings.internal_nucliadb_url
|
|
26
|
+
):
|
|
27
|
+
config.nucliadb.url = settings.internal_nucliadb_url
|
|
28
|
+
config.nucliadb.key = None
|
|
29
|
+
elif (
|
|
30
|
+
config.nucliadb is not None
|
|
31
|
+
and config.nucliadb.internal
|
|
32
|
+
and settings.internal_nucliadb_url is None
|
|
33
|
+
):
|
|
34
|
+
raise Exception("Internal NucliaDB URL not configured")
|
|
35
|
+
|
|
36
|
+
if session == "ephemeral":
|
|
37
|
+
memory = NoMemorySessionMemory(
|
|
38
|
+
config=MemoryConfig(nucliadb=None),
|
|
39
|
+
agent_id=agent,
|
|
40
|
+
workflow_id=workflow_id,
|
|
41
|
+
cache=cache,
|
|
42
|
+
)
|
|
43
|
+
elif config.nucliadb is None:
|
|
44
|
+
memory = EphemeralSessionMemory(
|
|
45
|
+
config=config, agent_id=agent, workflow_id=workflow_id, cache=cache
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
memory = SessionMemory(
|
|
49
|
+
config=config,
|
|
50
|
+
agent_id=agent,
|
|
51
|
+
workflow_id=workflow_id,
|
|
52
|
+
cache=cache,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
memory.init(session=session)
|
|
56
|
+
|
|
57
|
+
return memory
|
hyperforge/server/web.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import prometheus_client # type: ignore
|
|
2
|
+
from aiohttp import web
|
|
3
|
+
|
|
4
|
+
from hyperforge.server import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def http_handler(request: web.Request):
|
|
8
|
+
if request.path == "/metrics":
|
|
9
|
+
output = prometheus_client.exposition.generate_latest()
|
|
10
|
+
return web.Response(text=output.decode("utf8"))
|
|
11
|
+
elif request.path in ("/health/alive", "/health/ready"):
|
|
12
|
+
# implement health check here
|
|
13
|
+
return web.Response(text="OK")
|
|
14
|
+
else:
|
|
15
|
+
return web.Response(text="OK", status=404)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def start_web_server() -> web.Server:
|
|
19
|
+
server = web.Server(http_handler) # type: ignore
|
|
20
|
+
runner = web.ServerRunner(server)
|
|
21
|
+
await runner.setup()
|
|
22
|
+
site = web.TCPSite(runner, "0.0.0.0", 8000)
|
|
23
|
+
await site.start()
|
|
24
|
+
|
|
25
|
+
logger.info("======= Serving on http://0.0.0.0:8000/ ======")
|
|
26
|
+
return server
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def start_health_check():
|
|
30
|
+
server = await start_web_server()
|
|
31
|
+
return server
|
hyperforge/settings.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import model_validator
|
|
2
|
+
from pydantic_settings import BaseSettings
|
|
3
|
+
|
|
4
|
+
_REDIRECT_PATH = "/api/auth/agent/{agent_id}/workflow/{workflow_id}/session/{session_id}/oauth/{oauth_uuid}/callback"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OAuthSettings(BaseSettings):
|
|
8
|
+
nuclia_public_url: str = "https://{zone}.nuclia.com"
|
|
9
|
+
nuclia_zone: str = "arag"
|
|
10
|
+
rao_redirect_url: str = ""
|
|
11
|
+
|
|
12
|
+
@model_validator(mode="after")
|
|
13
|
+
def _resolve_urls(self) -> "OAuthSettings":
|
|
14
|
+
self.nuclia_public_url = self.nuclia_public_url.format(zone=self.nuclia_zone)
|
|
15
|
+
if not self.rao_redirect_url:
|
|
16
|
+
self.rao_redirect_url = self.nuclia_public_url.rstrip("/") + _REDIRECT_PATH
|
|
17
|
+
|
|
18
|
+
return self
|