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,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()
|