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,189 @@
1
+ """
2
+ StaticAgentManager — a drop-in replacement for AgentManager that serves agent
3
+ configuration from an in-memory StandaloneConfig rather than a PostgreSQL database.
4
+
5
+ Only the methods actually called at runtime (by SessionManager and the MCP/interaction
6
+ endpoints) are implemented. Management mutations (add/delete/patch) intentionally
7
+ raise NotImplementedError — the standalone deployment is read-only with respect to
8
+ agent configuration.
9
+ """
10
+
11
+ import datetime
12
+ from typing import Any, List
13
+
14
+ from hyperforge.db import exceptions
15
+ from hyperforge.models import MemoryConfig, Rules
16
+ from hyperforge.prompts import PromptConfig
17
+ from hyperforge.retrieval.config import RetrievalAgentConfig
18
+ from hyperforge.standalone.config import StandAloneAgentConfig
19
+ from hyperforge.workflows import RetrievalAgent, WorkflowData
20
+
21
+ _EPOCH = datetime.datetime.now()
22
+
23
+
24
+ class StaticAgentManager:
25
+ _config: dict[str, StandAloneAgentConfig]
26
+
27
+ def __init__(self, config: dict[str, StandAloneAgentConfig]) -> None:
28
+ self._config = config
29
+
30
+ # ------------------------------------------------------------------
31
+ # Lifecycle (no-ops — nothing to connect/disconnect)
32
+ # ------------------------------------------------------------------
33
+
34
+ async def initialize(self) -> None:
35
+ pass
36
+
37
+ async def finalize(self) -> None:
38
+ pass
39
+
40
+ # ------------------------------------------------------------------
41
+ # Internal helpers
42
+ # ------------------------------------------------------------------
43
+
44
+ def _get_agent(self, agent_id: str) -> StandAloneAgentConfig:
45
+ agent_config = self._config.get(agent_id)
46
+ if not agent_config:
47
+ raise exceptions.NotFoundError(f"Agent '{agent_id}' not found")
48
+ return agent_config
49
+
50
+ async def ensure_workflow_active(
51
+ self, account: str, agent_id: str, workflow_id: str
52
+ ) -> None:
53
+ agent_config = self._get_agent(agent_id)
54
+ if workflow_id not in agent_config.workflows:
55
+ raise exceptions.NotFoundError("Workflow not found")
56
+
57
+ def _build_retrieval_config(
58
+ self, agent_config: StandAloneAgentConfig, workflow_id: str = "default"
59
+ ) -> RetrievalAgentConfig:
60
+ """Merge StandAloneAgentConfig + WorkflowConfig into a RetrievalAgentConfig."""
61
+ workflow = agent_config.workflows.get(workflow_id)
62
+ if workflow is None:
63
+ raise exceptions.NotFoundError(
64
+ f"Workflow '{workflow_id}' not found in agent"
65
+ )
66
+ workflow_data = workflow.as_workflow_data(workflow_id)
67
+ # Merge agent-level rules with workflow-level rules (workflow takes precedence).
68
+ merged_rules = workflow.rules if workflow.rules.rules else agent_config.rules
69
+ return RetrievalAgentConfig(
70
+ drivers=agent_config.drivers,
71
+ rules=merged_rules,
72
+ memory=MemoryConfig(),
73
+ workflow=workflow_data,
74
+ preprocess=workflow.preprocess,
75
+ context=workflow.context,
76
+ generation=workflow.generation,
77
+ postprocess=workflow.postprocess,
78
+ )
79
+
80
+ # ------------------------------------------------------------------
81
+ # Methods used by SessionManager (session.py)
82
+ # ------------------------------------------------------------------
83
+
84
+ async def get_driver(self, account: str, agent_id: str, driver: str) -> Any:
85
+ agent_config = self._get_agent(agent_id)
86
+ for drv in agent_config.drivers:
87
+ if drv.identifier == driver:
88
+ return drv
89
+ raise exceptions.NotFoundError(
90
+ f"Driver '{driver}' not found in agent '{agent_id}'"
91
+ )
92
+
93
+ async def get_drivers(self, account: str, agent_id: str) -> List[Any]:
94
+ agent_config = self._get_agent(agent_id)
95
+ return list(agent_config.drivers)
96
+
97
+ async def workflows_list(self, account: str, agent_id: str) -> List[WorkflowData]:
98
+ agent_config = self._get_agent(agent_id)
99
+ return [
100
+ workflow.as_workflow_data(workflow_id)
101
+ for workflow_id, workflow in agent_config.workflows.items()
102
+ ]
103
+
104
+ async def get_agent_config_basic(
105
+ self, account: str, agent_id: str
106
+ ) -> RetrievalAgent:
107
+ agent_config = self._get_agent(agent_id)
108
+ # Use the default workflow for description/title fallbacks.
109
+ default_workflow = agent_config.workflows.get("default")
110
+ description = agent_config.description or (
111
+ default_workflow.description if default_workflow else None
112
+ )
113
+ title = agent_config.title or agent_id
114
+ return RetrievalAgent(
115
+ account=account,
116
+ agent_id=agent_id,
117
+ memory=None,
118
+ description=description,
119
+ title=title,
120
+ instructions=agent_config.instructions,
121
+ created=_EPOCH,
122
+ modified=_EPOCH,
123
+ )
124
+
125
+ async def get_agent_config(
126
+ self,
127
+ account: str,
128
+ agent_id: str,
129
+ internal_nucliadb_url: str | None = None,
130
+ default_memory: bool = False,
131
+ workflow_id: str = "default",
132
+ ) -> RetrievalAgentConfig:
133
+ agent_config = self._get_agent(agent_id)
134
+ return self._build_retrieval_config(agent_config, workflow_id)
135
+
136
+ async def get_prompt(
137
+ self, agent_id: str, account: str, prompt_id: str
138
+ ) -> PromptConfig:
139
+ agent_config = self._get_agent(agent_id)
140
+ for prompt in agent_config.prompts:
141
+ if prompt.prompt_id == prompt_id:
142
+ return prompt
143
+ raise exceptions.NotFoundError(
144
+ f"Prompt '{prompt_id}' not found in agent '{agent_id}'"
145
+ )
146
+
147
+ async def get_prompts(self, agent_id: str, account: str) -> List[PromptConfig]:
148
+ agent_config = self._get_agent(agent_id)
149
+ return list(agent_config.prompts)
150
+
151
+ async def get_rules(self, account: str, agent_id: str) -> Rules:
152
+ agent_config = self._get_agent(agent_id)
153
+ return agent_config.rules
154
+
155
+ async def get_preprocess(
156
+ self, account: str, agent_id: str, workflow_id: str = "default"
157
+ ) -> list:
158
+ agent_config = self._get_agent(agent_id)
159
+ workflow = agent_config.workflows.get(workflow_id)
160
+ if workflow is None:
161
+ return []
162
+ return workflow.preprocess or []
163
+
164
+ async def get_context(
165
+ self, account: str, agent_id: str, workflow_id: str = "default"
166
+ ) -> list:
167
+ agent_config = self._get_agent(agent_id)
168
+ workflow = agent_config.workflows.get(workflow_id)
169
+ if workflow is None:
170
+ return []
171
+ return workflow.context or []
172
+
173
+ async def get_generation(
174
+ self, account: str, agent_id: str, workflow_id: str = "default"
175
+ ) -> list:
176
+ agent_config = self._get_agent(agent_id)
177
+ workflow = agent_config.workflows.get(workflow_id)
178
+ if workflow is None:
179
+ return []
180
+ return workflow.generation or []
181
+
182
+ async def get_postprocess(
183
+ self, account: str, agent_id: str, workflow_id: str = "default"
184
+ ) -> list:
185
+ agent_config = self._get_agent(agent_id)
186
+ workflow = agent_config.workflows.get(workflow_id)
187
+ if workflow is None:
188
+ return []
189
+ return workflow.postprocess or []
@@ -0,0 +1,264 @@
1
+ """
2
+ Standalone application.
3
+
4
+ Runs the API (HTTP + MCP) and the agent runner (SessionManager) in the same
5
+ process, connected via a LocalBroker. No Redis, no gRPC, no PostgreSQL, no
6
+ NucliaDB required.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Tuple
11
+
12
+ import prometheus_client # type: ignore
13
+ from fastapi import APIRouter, FastAPI
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.staticfiles import StaticFiles
16
+ from lru import LRU
17
+ from mcp.server.lowlevel.server import Server as MCPServer
18
+ from mcp.server.streamable_http import StreamableHTTPServerTransport
19
+ from nucliadb_telemetry.logs import setup_logging
20
+ from nucliadb_telemetry.settings import LogLevel, LogSettings
21
+ from prometheus_client import CONTENT_TYPE_LATEST # type: ignore
22
+ from redis.asyncio import Redis
23
+ from starlette.authentication import AuthCredentials, AuthenticationBackend, BaseUser
24
+ from starlette.middleware.authentication import AuthenticationMiddleware
25
+ from starlette.requests import HTTPConnection
26
+ from starlette.responses import PlainTextResponse
27
+
28
+ from hyperforge.api import v1
29
+ from hyperforge.api.authentication import User
30
+ from hyperforge.api.models import AgentRole, StashRoles
31
+ from hyperforge.api.v1 import oauth as v1_oauth
32
+ from hyperforge.broker import Broker
33
+ from hyperforge.broker.local import LocalBroker
34
+ from hyperforge.broker.redis import RedisBroker
35
+ from hyperforge.configure import resolve_dotted_name
36
+ from hyperforge.server.cache import InMemoryCache, ValkeyCache
37
+ from hyperforge.server.session import SessionManager
38
+ from hyperforge.server.settings import Settings as ServerSettings
39
+ from hyperforge.standalone.settings import StandaloneSettings
40
+ from hyperforge.standalone.ui_router import router as ui_router
41
+
42
+ from .const import STANDALONE_ACCOUNT
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # SessionManager subclass that skips the aiohttp health-check server started
46
+ # by the base class — the FastAPI app already exposes /health/ready and
47
+ # /health/alive.
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ class StandaloneSessionManager(SessionManager):
52
+ """SessionManager without the embedded aiohttp metrics/health server."""
53
+
54
+ async def initialize(self, health_check: bool = False) -> None:
55
+ await super().initialize(health_check=health_check)
56
+
57
+
58
+ router = APIRouter()
59
+
60
+
61
+ @router.get("/metrics")
62
+ async def serve_metrics(): # pragma: no cover
63
+ output = prometheus_client.exposition.generate_latest()
64
+ return PlainTextResponse(
65
+ output.decode("utf8"), headers={"Content-Type": CONTENT_TYPE_LATEST}
66
+ )
67
+
68
+
69
+ @router.get("/health/ready")
70
+ async def health_ready():
71
+ return {"status": "ok"}
72
+
73
+
74
+ @router.get("/health/alive")
75
+ async def health_alive():
76
+ return {"status": "ok"}
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Open authentication backend — grants every request all roles so that the
81
+ # existing @requires_one decorators on interaction and MCP endpoints pass
82
+ # without an external authoriser.
83
+ # ---------------------------------------------------------------------------
84
+
85
+ _ALL_ROLES = AuthCredentials(
86
+ [
87
+ AgentRole.MEMBER,
88
+ StashRoles.OWNER,
89
+ StashRoles.MEMBER,
90
+ StashRoles.CONTRIBUTOR,
91
+ "READER",
92
+ "WRITER",
93
+ "MANAGER",
94
+ ]
95
+ )
96
+
97
+
98
+ class OpenAuthBackend(AuthenticationBackend):
99
+ """Authentication backend that accepts every request as a local user with
100
+ all roles. Only used in the standalone single-process deployment."""
101
+
102
+ async def authenticate(
103
+ self, conn: HTTPConnection
104
+ ) -> tuple[AuthCredentials, BaseUser] | None:
105
+ # Inject the headers that interaction endpoints declare as required
106
+ # FastAPI Header() dependencies, so they don't get rejected.
107
+ if "x-stf-account" not in conn.headers:
108
+ conn.scope["headers"] = [
109
+ (b"x-stf-account", STANDALONE_ACCOUNT.encode()),
110
+ (b"x-stf-user", b"standalone"),
111
+ (b"x-stf-account-type", b"v3starter"),
112
+ *[
113
+ (k, v)
114
+ for k, v in conn.scope["headers"]
115
+ if k
116
+ not in (
117
+ b"x-stf-account",
118
+ b"x-stf-user",
119
+ b"x-stf-account-type",
120
+ )
121
+ ],
122
+ ]
123
+ return _ALL_ROLES, User(username="standalone")
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Application
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ class StandaloneApplication(FastAPI):
132
+ """Single-process arag: API + SessionManager sharing one LocalBroker."""
133
+
134
+ def __init__(
135
+ self,
136
+ agents_cfg: dict[str, Any],
137
+ settings: StandaloneSettings,
138
+ **kwargs: Any,
139
+ ) -> None:
140
+ super().__init__(
141
+ title="arag standalone",
142
+ description="Single-process agent RAG — interaction and MCP only",
143
+ **kwargs,
144
+ )
145
+ self._agents_cfg = agents_cfg
146
+ self._standalone_settings = settings
147
+
148
+ # Only the interaction and MCP routes — no management endpoints.
149
+ self.include_router(v1.interaction.router)
150
+ self.include_router(v1.mcp_interaction.router)
151
+ self.include_router(v1_oauth.router)
152
+ self.include_router(router)
153
+ self.include_router(ui_router)
154
+
155
+ # Serve the built frontend SPA if the dist directory exists.
156
+ # In development the Vite dev server runs separately (proxied to :8080).
157
+ # app.py lives at: arag/src/hyperforge.standalone/app.py
158
+ # three .parent steps → arag/
159
+ _frontend_dist = Path(__file__).parent.parent.parent / "frontend" / "dist"
160
+ if _frontend_dist.is_dir():
161
+ self.mount(
162
+ "/",
163
+ StaticFiles(directory=str(_frontend_dist), html=True),
164
+ name="frontend",
165
+ )
166
+
167
+ self.add_middleware(AuthenticationMiddleware, backend=OpenAuthBackend())
168
+ self.add_middleware(
169
+ CORSMiddleware,
170
+ allow_credentials=True,
171
+ allow_origins=settings.cors_allow_origin,
172
+ allow_methods=["*"],
173
+ allow_headers=["*"],
174
+ )
175
+
176
+ self.add_event_handler("startup", self._startup)
177
+ self.add_event_handler("shutdown", self._shutdown)
178
+
179
+ # ------------------------------------------------------------------
180
+ # Property used by interaction.py / oauth.py to read answers_subject
181
+ # and oauth_subject without knowing about StandaloneSettings.
182
+ # ------------------------------------------------------------------
183
+
184
+ @property
185
+ def settings(self) -> ServerSettings:
186
+ return self._server_settings
187
+
188
+ # ------------------------------------------------------------------
189
+ # Lifecycle
190
+ # ------------------------------------------------------------------
191
+
192
+ async def _startup(self) -> None:
193
+ s = self._standalone_settings
194
+
195
+ setup_logging(
196
+ settings=LogSettings(
197
+ debug=s.debug,
198
+ log_level=LogLevel(s.log_level),
199
+ logger_levels={
200
+ "uvicorn.error": LogLevel.ERROR,
201
+ "mcp.client.streamable_http": LogLevel.WARNING,
202
+ "mcp.server.lowlevel.server": LogLevel.WARNING,
203
+ "hyperforge.configure": LogLevel.WARNING,
204
+ },
205
+ )
206
+ )
207
+
208
+ if s.broker_redis_dsn is None:
209
+ # Shared in-process broker — no Redis needed.
210
+ self.broker: Broker = LocalBroker(
211
+ keepalive_ms=int(s.pubsub_keepalive_seconds * 1000)
212
+ )
213
+ else:
214
+ self.broker = RedisBroker.from_url(
215
+ s.broker_redis_dsn,
216
+ s.broker_redis_activate_subject,
217
+ int(s.pubsub_keepalive_seconds * 1000),
218
+ cluster_mode=s.broker_redis_cluster_mode,
219
+ )
220
+
221
+ # LRU caches for MCP server instances (mirrors HTTPApplication).
222
+ self.sses: LRU[Tuple[str, str], StreamableHTTPServerTransport] = LRU(size=100)
223
+ self.mcp_servers: LRU[str, MCPServer] = LRU(size=100)
224
+
225
+ # Agent manager backed by the JSON config — no PostgreSQL.
226
+ agent_manager_class = resolve_dotted_name(s.agent_manager_class)
227
+ self.agent_manager: Any = agent_manager_class(self._agents_cfg)
228
+
229
+ # Build a ServerSettings instance so SessionManager and the subject
230
+ # format strings work unchanged. Redis settings are present but
231
+ # never used (LocalBroker is injected directly).
232
+ self._server_settings = ServerSettings(
233
+ valkey_url="redis://localhost",
234
+ question_timeout_seconds=s.question_timeout_seconds,
235
+ pubsub_keepalive_seconds=s.pubsub_keepalive_seconds,
236
+ internal_nua=s.internal_nua,
237
+ internal_nua_api=s.internal_nua_api,
238
+ external_nua_api_key=s.external_nua_api_key,
239
+ local_openai=s.local_openai,
240
+ internal_nucliadb=False,
241
+ internal_nucliadb_url=None,
242
+ standalone=True,
243
+ )
244
+
245
+ # use redis as cache backend if provided
246
+ cache: ValkeyCache | InMemoryCache
247
+ if s.session_cache_class is not None:
248
+ cache_class = resolve_dotted_name(s.session_cache_class)
249
+ cache = cache_class(s.session_cache_size)
250
+ elif s.broker_redis_dsn is not None:
251
+ redis_client: Redis = Redis.from_url(s.broker_redis_dsn) # type: ignore[arg-type]
252
+ cache = ValkeyCache(redis_client)
253
+ else:
254
+ cache = InMemoryCache(s.in_memory_cache_size)
255
+ self.session_manager: SessionManager = StandaloneSessionManager(
256
+ settings=self._server_settings,
257
+ broker=self.broker,
258
+ agent_manager=self.agent_manager,
259
+ cache=cache,
260
+ )
261
+ await self.session_manager.initialize()
262
+
263
+ async def _shutdown(self) -> None:
264
+ await self.session_manager.finalize()
@@ -0,0 +1,137 @@
1
+ """
2
+ Standalone agent configuration.
3
+
4
+ A single JSON file describing one or more agents. Runtime settings (NUA key,
5
+ HTTP host/port, log level, …) live in StandaloneSettings and are loaded from
6
+ environment variables or the command line — not from this file.
7
+
8
+ The top-level keys become the agent_id values used in API paths
9
+ (``/api/v1/agent/{agent_id}/...``). The account is always ``"local"`` in
10
+ standalone mode.
11
+
12
+ Minimal valid example
13
+ ---------------------
14
+ {
15
+ "my-agent": {
16
+ "workflows": {
17
+ "default": {
18
+ "name": "Default",
19
+ "context": [
20
+ {"module": "google", "title": "Google Search", "source": "google-01"}
21
+ ],
22
+ "generation": [
23
+ {"module": "summarize"}
24
+ ]
25
+ }
26
+ },
27
+ "drivers": [
28
+ {
29
+ "identifier": "google-01",
30
+ "name": "google",
31
+ "provider": "google",
32
+ "config": {"vertexai": false, "api_key": "..."}
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ """
38
+
39
+ from typing import Any, Dict, Optional
40
+
41
+ from pydantic import BaseModel, Field, TypeAdapter, field_serializer, field_validator
42
+
43
+ from hyperforge.agent import AgentConfig
44
+ from hyperforge.configure import get_agent_config_klass, get_driver_config_klass
45
+ from hyperforge.driver import DriverConfig
46
+ from hyperforge.models import Rules
47
+ from hyperforge.prompts import PromptConfig
48
+ from hyperforge.workflows import WorkflowData
49
+
50
+
51
+ class WorkflowConfig(BaseModel):
52
+ """Pipeline steps for a single named workflow."""
53
+
54
+ name: str = "default"
55
+ description: Optional[str] = None
56
+ parameters: Optional[dict[str, Any]] = None
57
+ required: list[str] = Field(default_factory=list)
58
+ rules: Rules = Field(default_factory=Rules)
59
+
60
+ preprocess: list = Field(default_factory=list)
61
+ context: list = Field(default_factory=list)
62
+ generation: list = Field(default_factory=list)
63
+ postprocess: list = Field(default_factory=list)
64
+
65
+ def as_workflow_data(self, workflow_id: str) -> WorkflowData:
66
+ return WorkflowData(
67
+ id=workflow_id,
68
+ name=self.name,
69
+ description=self.description,
70
+ parameters=self.parameters,
71
+ rules=self.rules,
72
+ required=self.required,
73
+ )
74
+
75
+ @field_validator(
76
+ "preprocess", "context", "generation", "postprocess", mode="before"
77
+ )
78
+ def validate_agents(cls, value: list[Dict[str, Any]], field):
79
+ if len(value) == 0:
80
+ return []
81
+ if all([isinstance(agent, AgentConfig) for agent in value]):
82
+ return value
83
+ result = []
84
+ for agent_config in value:
85
+ module = agent_config["module"]
86
+ agent_klass = get_agent_config_klass(module)
87
+ result.append(agent_klass.model_validate(agent_config))
88
+
89
+ return result
90
+
91
+ @field_serializer("preprocess", "context", "generation", "postprocess")
92
+ def serialize_agents(self, agents: list[AgentConfig]) -> list[Dict[str, Any]]:
93
+ return [agent.model_dump() for agent in agents]
94
+
95
+
96
+ class StandAloneAgentConfig(BaseModel):
97
+ """Configuration for a single agent instance."""
98
+
99
+ title: Optional[str] = None
100
+ description: Optional[str] = None
101
+ instructions: Optional[str] = None
102
+
103
+ # Drivers (LLM providers, search connectors, …) shared across all workflows.
104
+ # Use DriverConfig[Any] so the generic `config` field preserves arbitrary dicts.
105
+ drivers: list[DriverConfig[Any]] = Field(default_factory=list)
106
+
107
+ # Top-level rules applied to all workflows.
108
+ rules: Rules = Field(default_factory=Rules)
109
+
110
+ # Named workflows. A "default" entry is required for the default workflow.
111
+ workflows: dict[str, WorkflowConfig] = Field(default_factory=dict)
112
+
113
+ # Prompts exposed via the MCP server.
114
+ prompts: list[PromptConfig] = Field(default_factory=list)
115
+
116
+ @field_validator("drivers", mode="before")
117
+ def validate_drivers(cls, value: list[Dict[str, Any]], field):
118
+ if len(value) == 0:
119
+ return []
120
+ if all([isinstance(agent, DriverConfig) for agent in value]):
121
+ return value
122
+ result = []
123
+ for agent_config in value:
124
+ module = agent_config["provider"]
125
+ agent_klass = get_driver_config_klass(module)
126
+ result.append(agent_klass.model_validate(agent_config))
127
+
128
+ return result
129
+
130
+ @field_serializer("drivers")
131
+ def serialize_drivers(self, agents: list[DriverConfig]) -> list[Dict[str, Any]]:
132
+ return [agent.model_dump() for agent in agents]
133
+
134
+
135
+ # The config file is a plain JSON object whose keys are agent IDs.
136
+ # Use StandaloneConfig.validate_json(path.read_text()) to load it.
137
+ StandaloneConfig = TypeAdapter(dict[str, StandAloneAgentConfig])
@@ -0,0 +1 @@
1
+ STANDALONE_ACCOUNT = "local"
@@ -0,0 +1,60 @@
1
+ import sys
2
+
3
+ import uvicorn
4
+ import yaml
5
+ from hyperforge_standalone import logger
6
+ from hyperforge_standalone.app import StandaloneApplication
7
+ from hyperforge_standalone.config import StandAloneAgentConfig, StandaloneConfig
8
+ from hyperforge_standalone.settings import StandaloneSettings
9
+
10
+ from hyperforge.configure import resolve_dotted_name
11
+
12
+
13
+ def run(
14
+ application_class: type[StandaloneApplication] | None = None,
15
+ ) -> None: # pragma: no cover
16
+ settings: StandaloneSettings = StandaloneSettings() # type: ignore[call-arg]
17
+
18
+ if not settings.agents_config.exists():
19
+ print(
20
+ f"error: agents config file not found: {settings.agents_config}",
21
+ file=sys.stderr,
22
+ )
23
+ sys.exit(1)
24
+
25
+ from hyperforge.configure import load_all_configurations, scan
26
+
27
+ # Register all built-in agents and drivers (same as the base initialize,
28
+ # but without start_health_check() — the FastAPI app handles /health/*).
29
+ scan("nuclia_agents.agents.agents")
30
+ scan("nuclia_agents.drivers.drivers")
31
+ load_all_configurations("nuclia_agents")
32
+
33
+ for load_module in settings.load_modules:
34
+ try:
35
+ scan(load_module)
36
+ load_all_configurations(load_module)
37
+ except ImportError:
38
+ logger.error(f"Module {load_module} could not be loaded")
39
+
40
+ if settings.agents_config.suffix == ".yaml":
41
+ agents_cfg: dict[str, StandAloneAgentConfig] = StandaloneConfig.validate_python(
42
+ yaml.safe_load(settings.agents_config.read_text())
43
+ )
44
+ else:
45
+ agents_cfg = StandaloneConfig.validate_json(settings.agents_config.read_text())
46
+
47
+ if application_class is None:
48
+ application_class = resolve_dotted_name(settings.standalone_application_class)
49
+ app = application_class(agents_cfg, settings)
50
+
51
+ uvicorn.run(
52
+ app,
53
+ host=settings.host,
54
+ port=settings.port,
55
+ log_level=settings.log_level.lower(),
56
+ )
57
+
58
+
59
+ if __name__ == "__main__":
60
+ run()