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,82 @@
1
+ from typing import Dict, Optional
2
+
3
+ from redis.asyncio.cluster import RedisCluster
4
+
5
+
6
+ class ManualStreamKeysRedisCluster(RedisCluster):
7
+ """When connecting to a cluster, we need to know the key of each command to route it to the correct node. Stream functions (XREADGROUP, XREAD, etc) have a complicated syntax and the client does not extract the key, rather it sends the query to the server for parsing. If the server is down, this fails and the library does not have retries/reconnects for this code path. This can cause the client to get stuck and never reconnect. Since we know the keys, we can do this calculation ourselves to workaround this problem."""
8
+
9
+ def get_node_id_from_key(self, key: str):
10
+ keyslot = self.keyslot(key)
11
+ return self.nodes_manager.get_node_from_slot(keyslot)
12
+
13
+ @classmethod
14
+ def from_url(cls, url, **kwargs):
15
+ return super().from_url(
16
+ url=url,
17
+ **kwargs,
18
+ )
19
+
20
+ async def xreadgroup(
21
+ self,
22
+ groupname: str,
23
+ consumername: str,
24
+ streams: Dict[str, str],
25
+ block: int,
26
+ count: int,
27
+ noack: bool,
28
+ ):
29
+ # Get the node from the first stream key
30
+ if len(streams) != 1:
31
+ raise ValueError("Only one stream key is supported")
32
+ node = self.get_node_from_key(list(streams.keys())[0])
33
+ cmd_args = [
34
+ "XREADGROUP",
35
+ "GROUP",
36
+ groupname,
37
+ consumername,
38
+ ]
39
+ if noack:
40
+ cmd_args.append("NOACK")
41
+ cmd_args.extend(
42
+ [
43
+ "BLOCK",
44
+ str(block),
45
+ "COUNT",
46
+ str(count),
47
+ "STREAMS",
48
+ *streams.keys(),
49
+ *streams.values(),
50
+ ]
51
+ )
52
+
53
+ return await self.execute_command(*cmd_args, target_nodes=[node])
54
+
55
+ async def xread(
56
+ self,
57
+ streams: Dict[str, str],
58
+ block: int,
59
+ count: Optional[int] = None,
60
+ ):
61
+ # Get the node from the first stream key
62
+ # Get the node from the first stream key
63
+ if len(streams) != 1:
64
+ raise ValueError("Only one stream key is supported")
65
+ node = self.get_node_from_key(list(streams.keys())[0])
66
+
67
+ cmd_args = [
68
+ "XREAD",
69
+ "BLOCK",
70
+ str(block),
71
+ ]
72
+ if count is not None:
73
+ cmd_args.extend(["COUNT", str(count)])
74
+
75
+ cmd_args.extend(
76
+ [
77
+ "STREAMS",
78
+ *streams.keys(),
79
+ *streams.values(),
80
+ ]
81
+ )
82
+ return await self.execute_command(*cmd_args, target_nodes=[node])
File without changes
@@ -0,0 +1,169 @@
1
+ import asyncio
2
+ from functools import wraps
3
+ from typing import Callable, List, Literal, Optional
4
+ from uuid import uuid4
5
+
6
+ from sentry_sdk import capture_exception
7
+
8
+ from hyperforge import logger
9
+ from hyperforge.agent import Agent
10
+ from hyperforge.configure import get_agent_klass
11
+ from hyperforge.context.agent import ContextAgent
12
+ from hyperforge.manager import Manager
13
+ from hyperforge.memory.memory import QuestionMemory
14
+ from hyperforge.retrieval.config import RetrievalAgentConfig
15
+
16
+
17
+ def handle_stage_error(stage_name: str):
18
+ """Decorator to handle errors in retrieval agent stages"""
19
+
20
+ def decorator(func: Callable):
21
+ @wraps(func)
22
+ async def wrapper(self, memory: QuestionMemory, *args, **kwargs):
23
+ try:
24
+ return await func(self, memory, *args, **kwargs)
25
+ except Exception as e:
26
+ capture_exception(e)
27
+ logger.exception(f"Error in {stage_name}")
28
+ memory.final_answer = f"Error in {stage_name}"
29
+ raise # Re-raise to stop execution
30
+
31
+ return wrapper
32
+
33
+ return decorator
34
+
35
+
36
+ class RetrievalAgent(Agent):
37
+ module: Literal["retrieval"] = "retrieval"
38
+ debug: bool = False
39
+ preprocess: Optional[list[Agent]] = None
40
+ context: Optional[list[ContextAgent]] = None
41
+ generation: Optional[list[Agent]] = None
42
+ postprocess: Optional[list[Agent]] = None
43
+
44
+ def __init__(
45
+ self,
46
+ debug: bool = False,
47
+ preprocess: Optional[list[Agent]] = None,
48
+ context: Optional[list[ContextAgent]] = None,
49
+ generation: Optional[list[Agent]] = None,
50
+ postprocess: Optional[list[Agent]] = None,
51
+ ):
52
+ self.debug = debug
53
+ self.preprocess = preprocess or []
54
+ self.context = context or []
55
+ self.generation = generation or []
56
+ self.postprocess = postprocess or []
57
+
58
+ async def inner_from_config(self, config: object, agent_id: object = None) -> None: # type: ignore[override]
59
+ """No-op: RetrievalAgent is constructed via from_config_class, not from_config."""
60
+ pass
61
+
62
+ @classmethod
63
+ async def from_config_class(cls, config: RetrievalAgentConfig):
64
+ preprocess = []
65
+
66
+ for agent_obj in config.preprocess:
67
+ agent_class = get_agent_klass(agent_obj.module)
68
+ preprocess.append(await agent_class.from_config(agent_obj))
69
+
70
+ context: list[ContextAgent] = []
71
+ for context_agent_obj in config.context:
72
+ agent_class = get_agent_klass(context_agent_obj.module)
73
+ context.append(await agent_class.from_config(context_agent_obj)) # type: ignore[arg-type]
74
+
75
+ generation = []
76
+ for generation_agent_obj in config.generation:
77
+ agent_class = get_agent_klass(generation_agent_obj.module)
78
+ generation.append(await agent_class.from_config(generation_agent_obj))
79
+
80
+ postprocess = []
81
+ for post_agent_obj in config.postprocess:
82
+ agent_class = get_agent_klass(post_agent_obj.module)
83
+ postprocess.append(await agent_class.from_config(post_agent_obj))
84
+
85
+ return cls(
86
+ preprocess=preprocess,
87
+ context=context,
88
+ postprocess=postprocess,
89
+ generation=generation,
90
+ )
91
+
92
+ @handle_stage_error("preprocess")
93
+ async def _run_preprocess(self, memory: QuestionMemory, manager: Manager):
94
+ if self.preprocess:
95
+ await asyncio.gather(
96
+ *[
97
+ preprocess(memory=memory, manager=manager)
98
+ for preprocess in self.preprocess
99
+ ]
100
+ )
101
+
102
+ @handle_stage_error("context")
103
+ async def _run_context(self, memory: QuestionMemory, manager: Manager):
104
+ if self.context and self.debug is False:
105
+ questions: List[tuple[str, str]] = memory.get_questions()
106
+ # Launch all context agents for all questions in parallel
107
+ tasks = [
108
+ generation.get_question_context(
109
+ memory,
110
+ manager,
111
+ question_uuid=question_uuid,
112
+ question=question,
113
+ flow_id=str(uuid4()),
114
+ )
115
+ for question_uuid, question in questions
116
+ for generation in self.context
117
+ ]
118
+ await asyncio.gather(*tasks)
119
+ elif self.context and self.debug is True:
120
+ questions = memory.get_questions()
121
+ for generation in self.context:
122
+ for question_uuid, question in questions:
123
+ await generation.get_question_context(
124
+ memory,
125
+ manager,
126
+ question_uuid=question_uuid,
127
+ question=question,
128
+ flow_id=str(uuid4()),
129
+ )
130
+
131
+ @handle_stage_error("generation")
132
+ async def _run_generation(self, memory: QuestionMemory, manager: Manager):
133
+ if self.generation:
134
+ await asyncio.gather(
135
+ *[agent(memory, manager) for agent in self.generation],
136
+ )
137
+
138
+ @handle_stage_error("postprocess")
139
+ async def _run_postprocess(self, memory: QuestionMemory, manager: Manager):
140
+ if self.postprocess:
141
+ await asyncio.gather(
142
+ *[postprocess(memory, manager) for postprocess in self.postprocess],
143
+ )
144
+
145
+ async def __call__(
146
+ self,
147
+ memory: QuestionMemory,
148
+ manager: Manager,
149
+ ):
150
+ while memory.restart:
151
+ memory.restart = False
152
+
153
+ await self._run_preprocess(memory=memory, manager=manager)
154
+ if memory.secure is False:
155
+ memory.final_answer = "Insecure query"
156
+ break
157
+
158
+ await self._run_context(memory, manager)
159
+
160
+ if memory.final_answer is None:
161
+ await self._run_generation(memory, manager)
162
+
163
+ if memory.restart is False:
164
+ await memory.add_final_answer()
165
+ await self._run_postprocess(memory, manager)
166
+
167
+ if memory.secure is False:
168
+ memory.final_answer = "Insecure context retrieved"
169
+ break
@@ -0,0 +1,94 @@
1
+ import datetime
2
+ from typing import Annotated, Any, Dict, Literal, Union
3
+
4
+ from pydantic import BaseModel, Field, TypeAdapter, field_serializer, field_validator
5
+
6
+ from hyperforge.agent import AgentConfig
7
+ from hyperforge.configure import get_agent_config_klass, get_driver_config_klass
8
+ from hyperforge.driver import DriverConfig
9
+ from hyperforge.models import MemoryConfig, Rules
10
+ from hyperforge.prompts import PromptConfig
11
+ from hyperforge.workflows import WorkflowData
12
+
13
+
14
+ class RetrievalAgentConfig(BaseModel):
15
+ drivers: list[DriverConfig]
16
+ rules: Rules
17
+ memory: MemoryConfig
18
+ workflow: WorkflowData
19
+
20
+ preprocess: list[AgentConfig]
21
+ context: list[AgentConfig]
22
+ generation: list[AgentConfig]
23
+ postprocess: list[AgentConfig]
24
+
25
+ @field_serializer("preprocess", "context", "generation", "postprocess", "drivers")
26
+ def serialize_agents(self, agents: list[AgentConfig]) -> list[Dict[str, Any]]:
27
+ return [agent.model_dump() for agent in agents]
28
+
29
+ @field_validator("drivers", mode="before")
30
+ def validate_drivers(cls, value: list[Dict[str, Any]], field):
31
+ if len(value) == 0:
32
+ return []
33
+ if all([isinstance(agent, DriverConfig) for agent in value]):
34
+ return value
35
+ result = []
36
+ for agent_config in value:
37
+ module = agent_config["provider"]
38
+ agent_klass = get_driver_config_klass(module)
39
+ result.append(agent_klass.model_validate(agent_config))
40
+
41
+ return result
42
+
43
+ @field_validator(
44
+ "preprocess", "context", "generation", "postprocess", mode="before"
45
+ )
46
+ def validate_agents(cls, value: list[Dict[str, Any]], field):
47
+ if len(value) == 0:
48
+ return []
49
+ if all([isinstance(agent, AgentConfig) for agent in value]):
50
+ return value
51
+ result = []
52
+ for agent_config in value:
53
+ module = agent_config["module"]
54
+ agent_klass = get_agent_config_klass(module)
55
+ result.append(agent_klass.model_validate(agent_config))
56
+
57
+ return result
58
+
59
+ def is_empty(self) -> bool:
60
+ return (
61
+ len(self.drivers) == 0
62
+ and len(self.preprocess) == 0
63
+ and len(self.context) == 0
64
+ and len(self.generation) == 0
65
+ and len(self.postprocess) == 0
66
+ )
67
+
68
+
69
+ class RetrievalAgentExportV1(BaseModel):
70
+ version: Literal["1"] = Field(default="1")
71
+ agent_config: RetrievalAgentConfig | None
72
+ agent_config_workflows: dict[str, RetrievalAgentConfig] = Field(
73
+ default_factory=dict
74
+ )
75
+ prompts: list[PromptConfig]
76
+ timestamp: str = Field(
77
+ default_factory=lambda: datetime.datetime.now(datetime.UTC).isoformat(
78
+ timespec="seconds"
79
+ ),
80
+ description="Timestamp of when the export was created in UTC",
81
+ )
82
+
83
+
84
+ RetrievalAgentExport = Annotated[
85
+ Union[RetrievalAgentExportV1], Field(discriminator="version")
86
+ ]
87
+ retrievalAgentAdapter = TypeAdapter(RetrievalAgentExport)
88
+
89
+
90
+ class RetrievalAgentExportRequest(BaseModel):
91
+ passphrase: str = Field(
92
+ description="Passphrase to encrypt the exported configuration. Will be required for import.",
93
+ min_length=16,
94
+ )
@@ -0,0 +1,5 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("hyperforge.server")
4
+
5
+ SERVICE_NAME = "hyperforge.server"
@@ -0,0 +1,131 @@
1
+ from lru import LRU
2
+ from redis.asyncio import Redis
3
+
4
+ from hyperforge.models import HistoryQuestionAnswer, Source
5
+
6
+
7
+ class Cache:
8
+ async def get(self, key: str) -> str | None:
9
+ raise NotImplementedError()
10
+
11
+ async def set(self, key: str, value: str, expire: int):
12
+ raise NotImplementedError()
13
+
14
+ async def get_list(self, key: str) -> list[str] | None:
15
+ raise NotImplementedError()
16
+
17
+ async def append(self, key: str, values: list[str], limit: int, expire: int):
18
+ raise NotImplementedError()
19
+
20
+
21
+ class NoCache(Cache):
22
+ async def get(self, key: str) -> str | None:
23
+ return None
24
+
25
+ async def get_list(self, key: str) -> list[str] | None:
26
+ return None
27
+
28
+ async def set(self, key: str, value: str, expire: int):
29
+ pass
30
+
31
+ async def append(self, key: str, values: list[str], limit: int, expire: int):
32
+ pass
33
+
34
+
35
+ class InMemoryCache(Cache):
36
+ _store: LRU
37
+ _list_store: LRU
38
+
39
+ def __init__(self, size: int = 3000):
40
+ self._store = LRU(size)
41
+ self._list_store = LRU(size)
42
+
43
+ async def get(self, key: str) -> str | None:
44
+ return self._store.get(key)
45
+
46
+ async def get_list(self, key: str) -> list[str] | None:
47
+ return self._list_store.get(key)
48
+
49
+ async def set(self, key: str, value: str, expire: int):
50
+ self._store[key] = value
51
+
52
+ async def append(self, key: str, values: list[str], limit: int, expire: int):
53
+ if key not in self._list_store:
54
+ self._list_store[key] = []
55
+ self._list_store[key].extend(values)
56
+ # Enforce limit
57
+ if len(self._list_store[key]) > limit:
58
+ self._list_store[key] = self._list_store[key][-limit:]
59
+
60
+
61
+ class ValkeyCache(Cache):
62
+ def __init__(self, client: Redis):
63
+ self.client = client
64
+
65
+ async def get(self, key: str) -> str | None:
66
+ return await self.client.get(key)
67
+
68
+ async def get_list(self, key: str) -> list[str] | None:
69
+ list = await self.client.lrange(key, 0, -1)
70
+ if len(list) == 0:
71
+ # This can mean empty list or non-cached, be specific in return
72
+ if await self.client.exists(key):
73
+ return []
74
+ else:
75
+ return None
76
+ else:
77
+ return list
78
+
79
+ async def set(self, key: str, value: str, expire: int):
80
+ await self.client.set(key, value, ex=expire)
81
+
82
+ async def append(self, key: str, values: list[str], limit: int, expire: int):
83
+ async with self.client.pipeline() as multi:
84
+ await (
85
+ multi.rpush(key, *values)
86
+ .ltrim(key, -limit, -1)
87
+ .expire(key, expire)
88
+ .execute()
89
+ )
90
+
91
+
92
+ # The following are small wrapper classes for each cache item, to ensure they are
93
+ # always accessed in a consistent way.
94
+ #
95
+ # Key design notes:
96
+ # - Components separated by dot (.)
97
+ # - From greater to lesser specificity (so it's possible to invalidate by prefix)
98
+ # - Keys include descriptive components before IDs so it's easy to see what a random hex number means
99
+ class CachedNucliaDBSource:
100
+ def __init__(self, cache: Cache, agent_id: str, source: str):
101
+ self._cache = cache
102
+ self._key = f"cache.agent.{agent_id}.source.{source}"
103
+
104
+ async def get(self) -> Source | None:
105
+ value = await self._cache.get(self._key)
106
+ if value is None:
107
+ return None
108
+ return Source.model_validate_json(value)
109
+
110
+ async def set(self, source: Source):
111
+ await self._cache.set(self._key, source.model_dump_json(), expire=900)
112
+
113
+
114
+ class CachedSessionQA:
115
+ def __init__(self, cache: Cache, agent_id: str, session: str):
116
+ self._cache = cache
117
+ self._key = f"cache.agent.{agent_id}.session.{session}.qa_history"
118
+
119
+ async def get(self) -> list[HistoryQuestionAnswer] | None:
120
+ value = await self._cache.get_list(self._key)
121
+ if value is None:
122
+ return None
123
+ return [HistoryQuestionAnswer.model_validate_json(v) for v in value]
124
+
125
+ async def append(self, qa: HistoryQuestionAnswer):
126
+ await self.append_all([qa])
127
+
128
+ async def append_all(self, qas: list[HistoryQuestionAnswer]):
129
+ await self._cache.append(
130
+ self._key, [qa.model_dump_json() for qa in qas], limit=20, expire=3600
131
+ )
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ from importlib.metadata import version
3
+ from typing import Optional
4
+
5
+ import sentry_sdk
6
+ from hyperforge_database.agents import AgentManager
7
+ from hyperforge_database.settings import DataManagerSettings
8
+ from hyperforge_server import SERVICE_NAME
9
+ from hyperforge_server.cache import ValkeyCache
10
+ from hyperforge_server.session import SessionManager
11
+ from hyperforge_server.settings import Settings
12
+ from nucliadb_telemetry.fastapi import application_metrics
13
+ from nucliadb_telemetry.logs import setup_logging
14
+ from nucliadb_telemetry.settings import LogLevel, LogSettings
15
+ from nucliadb_telemetry.tracerprovider import AsyncTracerProvider
16
+ from nucliadb_telemetry.utils import get_telemetry, setup_telemetry
17
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
18
+
19
+ from hyperforge.broker.redis import RedisBroker
20
+
21
+
22
+ def set_sentry(zone: str, environment: str, sentry_url: str):
23
+ sentry_exception = ExcepthookIntegration(always_run=True)
24
+ sentry_sdk.init(
25
+ release=version("hyperforge"),
26
+ environment=environment,
27
+ dsn=sentry_url,
28
+ integrations=[sentry_exception],
29
+ )
30
+ sentry_sdk.set_tag("zone", zone)
31
+
32
+
33
+ async def run_metrics_server(port: int):
34
+ import uvicorn
35
+
36
+ config = uvicorn.Config(
37
+ application_metrics, host="0.0.0.0", port=port, log_level="info"
38
+ )
39
+ server = uvicorn.Server(config)
40
+ await server.serve()
41
+
42
+
43
+ async def run_server(
44
+ settings: Settings,
45
+ tracer: Optional[AsyncTracerProvider],
46
+ data_manager_settings: DataManagerSettings,
47
+ ) -> SessionManager:
48
+ if tracer:
49
+ await setup_telemetry(SERVICE_NAME)
50
+ # Connect to Valkey
51
+ broker = RedisBroker.from_url(
52
+ url=settings.valkey_url,
53
+ activate_subject=settings.activate_subject,
54
+ keepalive_ms=int(settings.pubsub_keepalive_seconds * 1000),
55
+ cluster_mode=settings.valkey_cluster_mode,
56
+ )
57
+
58
+ agent_manager = await AgentManager.from_settings(
59
+ settings=data_manager_settings,
60
+ )
61
+ await agent_manager.initialize()
62
+
63
+ session = SessionManager(
64
+ settings=settings,
65
+ broker=broker,
66
+ agent_manager=agent_manager,
67
+ cache=ValkeyCache(broker._client),
68
+ )
69
+
70
+ return session
71
+
72
+
73
+ def run(): # pragma: no cover
74
+ settings = Settings()
75
+ setup_logging(
76
+ settings=LogSettings(
77
+ debug=settings.debug,
78
+ log_level=LogLevel(settings.log_level),
79
+ logger_levels={
80
+ "uvicorn.error": LogLevel.ERROR,
81
+ "nucliadb_telemetry": LogLevel.ERROR,
82
+ "mcp.client.streamable_http": LogLevel.WARNING,
83
+ "mcp.server.lowlevel.server": LogLevel.WARNING,
84
+ "hyperforge.configure": LogLevel.WARNING,
85
+ },
86
+ )
87
+ )
88
+ data_manager_settings = DataManagerSettings()
89
+ tracer = get_telemetry("nuclia-arag-server")
90
+ if settings.sentry_url is not None:
91
+ set_sentry(
92
+ settings.zone,
93
+ settings.running_environment,
94
+ settings.sentry_url,
95
+ )
96
+ loop = asyncio.get_event_loop()
97
+
98
+ loop.create_task(run_metrics_server(settings.metrics_port))
99
+
100
+ session = loop.run_until_complete(
101
+ run_server(settings, tracer, data_manager_settings)
102
+ )
103
+ loop.run_until_complete(session.initialize())
104
+ try:
105
+ loop.run_forever()
106
+ finally:
107
+ loop.run_until_complete(session.finalize())
108
+ loop.run_until_complete(loop.shutdown_asyncgens())
109
+ loop.close()
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ from importlib.metadata import version
3
+
4
+ import sentry_sdk
5
+ from nucliadb_telemetry.fastapi import application_metrics
6
+ from nucliadb_telemetry.logs import setup_logging
7
+ from nucliadb_telemetry.settings import LogLevel, LogSettings
8
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
9
+
10
+ from agents.restricted.src.hyperforge_restricted import sandbox
11
+ from hyperforge.server.settings import Settings
12
+
13
+
14
+ def set_sentry(zone: str, environment: str, sentry_url: str):
15
+ sentry_exception = ExcepthookIntegration(always_run=True)
16
+ sentry_sdk.init(
17
+ release=version("hyperforge"),
18
+ environment=environment,
19
+ dsn=sentry_url,
20
+ integrations=[sentry_exception],
21
+ )
22
+ sentry_sdk.set_tag("zone", zone)
23
+
24
+
25
+ async def run_metrics_server(port: int):
26
+ import uvicorn
27
+
28
+ config = uvicorn.Config(
29
+ application_metrics, host="0.0.0.0", port=port, log_level="info"
30
+ )
31
+ server = uvicorn.Server(config)
32
+ await server.serve()
33
+
34
+
35
+ def run(): # pragma: no cover
36
+ settings = Settings()
37
+ sandbox_settings = sandbox.SandboxSettings()
38
+ setup_logging(
39
+ settings=LogSettings(
40
+ debug=settings.debug,
41
+ log_level=LogLevel(settings.log_level),
42
+ logger_levels={
43
+ "uvicorn.error": LogLevel.ERROR,
44
+ "nucliadb_telemetry": LogLevel.ERROR,
45
+ "mcp.client.streamable_http": LogLevel.WARNING,
46
+ "mcp.server.lowlevel.server": LogLevel.WARNING,
47
+ "hyperforge.configure": LogLevel.WARNING,
48
+ },
49
+ )
50
+ )
51
+
52
+ loop = asyncio.get_event_loop()
53
+
54
+ loop.create_task(run_metrics_server(sandbox_settings.sandbox_metrics_port))
55
+ loop.create_task(sandbox.run_sandbox_server())
56
+ try:
57
+ loop.run_forever()
58
+ finally:
59
+ loop.run_until_complete(loop.shutdown_asyncgens())
60
+ loop.close()