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,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime settings for the standalone arag deployment.
|
|
3
|
+
|
|
4
|
+
All values can be supplied via:
|
|
5
|
+
- Environment variables (prefixed with ARAG_, e.g. ARAG_EXTERNAL_NUA_API_KEY)
|
|
6
|
+
- A .env file
|
|
7
|
+
- CLI arguments when using the pydantic-settings CLI integration
|
|
8
|
+
|
|
9
|
+
The agent pipeline definition (drivers, workflows, etc.) lives separately in
|
|
10
|
+
a JSON config file pointed to by ``agents_config``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from pydantic import AliasChoices, Field
|
|
17
|
+
from pydantic_settings import BaseSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StandaloneSettings(BaseSettings):
|
|
21
|
+
# ------------------------------------------------------------------
|
|
22
|
+
# Agent config file
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
agents_config: Path = Field(
|
|
26
|
+
default=Path("agents_config.yaml"),
|
|
27
|
+
description="Path to the JSON file containing agent definitions.",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# HTTP server
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
host: str = Field(default="0.0.0.0", description="Listen host.")
|
|
35
|
+
port: int = Field(default=8080, description="Listen port.")
|
|
36
|
+
log_level: str = Field(default="INFO", description="Log level (uvicorn + app).")
|
|
37
|
+
debug: bool = Field(default=False, description="Enable debug mode.")
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Agent runner
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
question_timeout_seconds: int = Field(
|
|
44
|
+
default=300,
|
|
45
|
+
description="Maximum seconds allowed to answer a single question.",
|
|
46
|
+
)
|
|
47
|
+
pubsub_keepalive_seconds: float = Field(
|
|
48
|
+
default=20,
|
|
49
|
+
description="Interval between keepalive pings on the answer stream.",
|
|
50
|
+
)
|
|
51
|
+
broker_redis_dsn: Optional[str] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description=(
|
|
54
|
+
"Redis DSN for the Pub/Sub broker. If not set, an in-memory broker is used, "
|
|
55
|
+
"which is not suitable for production but fine for local testing and development."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
broker_redis_activate_subject: str = Field(
|
|
59
|
+
default="arag:activations",
|
|
60
|
+
description="Redis stream subject for agent activations (only used if broker_redis_dsn is set).",
|
|
61
|
+
)
|
|
62
|
+
broker_redis_cluster_mode: bool = Field(
|
|
63
|
+
default=False,
|
|
64
|
+
description="Whether to use Redis Cluster mode (only used if broker_redis_dsn is set).",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# NUA / predict engine
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
external_nua_api_key: Optional[str] = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
description=(
|
|
74
|
+
"NUA API key from https://nuclia.cloud/user/keys. "
|
|
75
|
+
"Required unless internal_nua=true or local_openai is set."
|
|
76
|
+
),
|
|
77
|
+
# also allow external_nua_api_key and nua_api_key
|
|
78
|
+
# for backward compatibility with older env var names
|
|
79
|
+
validation_alias=AliasChoices(
|
|
80
|
+
"EXTERNAL_NUA_API_KEY",
|
|
81
|
+
"external_nua_api_key",
|
|
82
|
+
"NUA_API_KEY",
|
|
83
|
+
"nua_api_key",
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
internal_nua: bool = Field(
|
|
87
|
+
default=False,
|
|
88
|
+
description="Connect to an internal NUA service instead of nuclia.cloud.",
|
|
89
|
+
)
|
|
90
|
+
internal_nua_api: str = Field(
|
|
91
|
+
default="http://predict.learning.svc.cluster.local:8080",
|
|
92
|
+
description="Internal NUA service address (only used when internal_nua=true).",
|
|
93
|
+
)
|
|
94
|
+
local_openai: Optional[str] = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
description=(
|
|
97
|
+
"Base URL of a local OpenAI-compatible inference server "
|
|
98
|
+
"(e.g. http://localhost:11434/v1). "
|
|
99
|
+
"When set, requests are routed there instead of nuclia.cloud."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
in_memory_cache_size: int = 3000
|
|
104
|
+
|
|
105
|
+
cors_allow_origin: list[str] = Field(
|
|
106
|
+
default_factory=lambda: ["*"],
|
|
107
|
+
description=(
|
|
108
|
+
"List of allowed origins for CORS. Defaults to ['*'] which allows all origins. "
|
|
109
|
+
"In production, it's recommended to set this to the specific origins that should be allowed."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
load_modules: list[str] = []
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Pluggable module classes
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
session_cache_class: str | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description="Dotted-name of the session cache class to use.",
|
|
121
|
+
)
|
|
122
|
+
session_cache_size: int = Field(
|
|
123
|
+
default=3000,
|
|
124
|
+
description="Size of the session cache when using a custom session cache class.",
|
|
125
|
+
)
|
|
126
|
+
agent_manager_class: str = Field(
|
|
127
|
+
default="hyperforge.standalone.agent.StaticAgentManager",
|
|
128
|
+
description="Dotted-name of the AgentManager class to use.",
|
|
129
|
+
)
|
|
130
|
+
standalone_application_class: str = Field(
|
|
131
|
+
default="hyperforge.standalone.app.StandaloneApplication",
|
|
132
|
+
description="Dotted-name of the StandaloneApplication class to use.",
|
|
133
|
+
)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI API router for the standalone ARAG deployment.
|
|
3
|
+
|
|
4
|
+
Provides endpoints consumed exclusively by the built-in frontend:
|
|
5
|
+
GET /api/v1/ui/schema — all registered agent/driver schemas
|
|
6
|
+
GET /api/v1/ui/config — current in-memory agent config (full JSON)
|
|
7
|
+
PUT /api/v1/ui/config — replace config, persist to AGENTS_CONFIG file
|
|
8
|
+
GET /api/v1/ui/models — list of known model IDs for model_select widget
|
|
9
|
+
|
|
10
|
+
No authentication is required (the OpenAuthBackend already grants all roles
|
|
11
|
+
in standalone mode, and the UI is local-only by design).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
22
|
+
from fastapi.responses import JSONResponse
|
|
23
|
+
|
|
24
|
+
from hyperforge.standalone.config import StandaloneConfig
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
router = APIRouter()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Schema — expose the registered agent/driver registry so the UI can build
|
|
33
|
+
# the "add agent" palette and the config form for each agent type.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Properties that link to other agents (subagents). The frontend renders these
|
|
38
|
+
# as edges/child nodes on the canvas instead of inline form fields.
|
|
39
|
+
CONNECTABLE_KEYS: list[str] = [
|
|
40
|
+
"fallback",
|
|
41
|
+
"next_agent",
|
|
42
|
+
"then",
|
|
43
|
+
"else_",
|
|
44
|
+
"agents",
|
|
45
|
+
"registered_agents",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _merge_defs(target: dict[str, Any], source: dict[str, Any]) -> None:
|
|
50
|
+
"""Merge `source["$defs"]` into target without overwriting existing keys."""
|
|
51
|
+
defs = source.get("$defs") or {}
|
|
52
|
+
for name, schema in defs.items():
|
|
53
|
+
target.setdefault(name, schema)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _module_to_def_name(config_schema: dict[str, Any]) -> str | None:
|
|
57
|
+
"""Return the $defs key name for an agent config schema (= its `title`)."""
|
|
58
|
+
return config_schema.get("title")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get("/api/v1/ui/schema", response_class=JSONResponse)
|
|
62
|
+
async def get_schema() -> dict[str, Any]:
|
|
63
|
+
"""Return all registered agent and driver schemas plus a merged $defs map.
|
|
64
|
+
|
|
65
|
+
Payload shape:
|
|
66
|
+
{
|
|
67
|
+
"preprocess": { module_id: AgentSchema, ... },
|
|
68
|
+
"context": { ... },
|
|
69
|
+
"generation": { ... },
|
|
70
|
+
"postprocess":{ ... },
|
|
71
|
+
"drivers": { provider_id: DriverSchema, ... },
|
|
72
|
+
"$defs": { ClassName: JsonSchema, ... }, # merged from all agents
|
|
73
|
+
"connectable_keys": [...],
|
|
74
|
+
"agent_module_to_def": { module_id: ClassName, ... }
|
|
75
|
+
}
|
|
76
|
+
"""
|
|
77
|
+
from hyperforge.configure import (
|
|
78
|
+
get_context_agent_schemas,
|
|
79
|
+
get_driver_agent_schemas,
|
|
80
|
+
get_generation_agent_schemas,
|
|
81
|
+
get_postprocess_agent_schemas,
|
|
82
|
+
get_preprocess_agent_schemas,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
stages = {
|
|
86
|
+
"preprocess": get_preprocess_agent_schemas(),
|
|
87
|
+
"context": get_context_agent_schemas(),
|
|
88
|
+
"generation": get_generation_agent_schemas(),
|
|
89
|
+
"postprocess": get_postprocess_agent_schemas(),
|
|
90
|
+
}
|
|
91
|
+
drivers = get_driver_agent_schemas()
|
|
92
|
+
|
|
93
|
+
# Merge all $defs into a single top-level map so the frontend can resolve
|
|
94
|
+
# `$ref: "#/$defs/<ClassName>"` across stages/drivers.
|
|
95
|
+
merged_defs: dict[str, Any] = {}
|
|
96
|
+
agent_module_to_def: dict[str, str] = {}
|
|
97
|
+
|
|
98
|
+
for stage_agents in stages.values():
|
|
99
|
+
for module_id, agent_schema in stage_agents.items():
|
|
100
|
+
cfg = agent_schema.get("config_schema") or {}
|
|
101
|
+
_merge_defs(merged_defs, cfg)
|
|
102
|
+
# The top-level config schema itself is also exposed under $defs so
|
|
103
|
+
# nested oneOf/discriminator chains can resolve it.
|
|
104
|
+
def_name = _module_to_def_name(cfg)
|
|
105
|
+
if def_name:
|
|
106
|
+
merged_defs.setdefault(def_name, cfg)
|
|
107
|
+
agent_module_to_def[module_id] = def_name
|
|
108
|
+
|
|
109
|
+
for driver_schema in drivers.values():
|
|
110
|
+
cfg = driver_schema.get("config_schema") or {}
|
|
111
|
+
_merge_defs(merged_defs, cfg)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
**stages,
|
|
115
|
+
"drivers": drivers,
|
|
116
|
+
"$defs": merged_defs,
|
|
117
|
+
"connectable_keys": CONNECTABLE_KEYS,
|
|
118
|
+
"agent_module_to_def": agent_module_to_def,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Config — read / write the full agent config
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get("/api/v1/ui/config", response_class=JSONResponse)
|
|
128
|
+
async def get_config(request: Request) -> dict[str, Any]:
|
|
129
|
+
"""Return the current in-memory config as a JSON-serialisable dict."""
|
|
130
|
+
agent_manager = request.app.agent_manager
|
|
131
|
+
return {
|
|
132
|
+
agent_id: agent_cfg.model_dump(mode="json")
|
|
133
|
+
for agent_id, agent_cfg in agent_manager._config.items()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.put("/api/v1/ui/config", response_class=JSONResponse)
|
|
138
|
+
async def put_config(request: Request) -> dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Replace the in-memory config and persist it to the config file on disk.
|
|
141
|
+
|
|
142
|
+
The request body must be a JSON object whose structure matches the
|
|
143
|
+
standalone config format (same as AGENTS_CONFIG).
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
body = await request.json()
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
# Validate through the Pydantic model so we get clear error messages.
|
|
151
|
+
try:
|
|
152
|
+
new_config = StandaloneConfig.validate_python(body)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
155
|
+
|
|
156
|
+
# Update the live in-memory config.
|
|
157
|
+
agent_manager = request.app.agent_manager
|
|
158
|
+
agent_manager._config = new_config
|
|
159
|
+
|
|
160
|
+
# Persist to disk so the change survives a restart.
|
|
161
|
+
settings = request.app._standalone_settings
|
|
162
|
+
config_path = settings.agents_config
|
|
163
|
+
try:
|
|
164
|
+
serialised = {
|
|
165
|
+
agent_id: agent_cfg.model_dump(mode="json")
|
|
166
|
+
for agent_id, agent_cfg in new_config.items()
|
|
167
|
+
}
|
|
168
|
+
if config_path.suffix in (".yaml", ".yml"):
|
|
169
|
+
config_path.write_text(yaml.dump(serialised, allow_unicode=True))
|
|
170
|
+
else:
|
|
171
|
+
config_path.write_text(json.dumps(serialised, indent=2, ensure_ascii=False))
|
|
172
|
+
logger.info("Config persisted to %s", config_path)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
# Non-fatal — in-memory update already succeeded.
|
|
175
|
+
logger.warning("Could not persist config to %s: %s", config_path, exc)
|
|
176
|
+
|
|
177
|
+
return {"status": "ok", "agents": list(new_config.keys())}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Models — return a list of known model IDs for the model_select widget.
|
|
182
|
+
# Collects defaults from all registered agent config schemas where the field
|
|
183
|
+
# carries widget="model_select", then merges with a hard-coded baseline list.
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
_BASELINE_MODELS: list[str] = [
|
|
187
|
+
"chatgpt-azure-4o-mini",
|
|
188
|
+
"chatgpt-azure-4o",
|
|
189
|
+
"chatgpt4o",
|
|
190
|
+
"chatgpt-4.1",
|
|
191
|
+
"chatgpt-5",
|
|
192
|
+
"chatgpt-o3-mini",
|
|
193
|
+
"gemini-2.5-flash",
|
|
194
|
+
"gemini-2.5-flash-lite",
|
|
195
|
+
"gemini-3-flash-preview",
|
|
196
|
+
"claude-4-5-haiku",
|
|
197
|
+
"claude-4-5-sonnet",
|
|
198
|
+
"gcp-claude-4-5-haiku",
|
|
199
|
+
"gcp-claude-4-5-sonnet",
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _collect_model_defaults() -> list[str]:
|
|
204
|
+
"""Walk all agent config schemas and harvest model field defaults."""
|
|
205
|
+
from hyperforge.configure import (
|
|
206
|
+
get_context_agent_schemas,
|
|
207
|
+
get_driver_agent_schemas,
|
|
208
|
+
get_generation_agent_schemas,
|
|
209
|
+
get_postprocess_agent_schemas,
|
|
210
|
+
get_preprocess_agent_schemas,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
seen: set[str] = set(_BASELINE_MODELS)
|
|
214
|
+
|
|
215
|
+
all_schemas = [
|
|
216
|
+
*get_preprocess_agent_schemas().values(),
|
|
217
|
+
*get_context_agent_schemas().values(),
|
|
218
|
+
*get_generation_agent_schemas().values(),
|
|
219
|
+
*get_postprocess_agent_schemas().values(),
|
|
220
|
+
*get_driver_agent_schemas().values(),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
for agent_schema in all_schemas:
|
|
224
|
+
config_schema = agent_schema.get("config_schema", {})
|
|
225
|
+
for _field_name, field_schema in config_schema.get("properties", {}).items():
|
|
226
|
+
if field_schema.get("widget") == "model_select":
|
|
227
|
+
default = field_schema.get("default")
|
|
228
|
+
if isinstance(default, str) and default:
|
|
229
|
+
seen.add(default)
|
|
230
|
+
|
|
231
|
+
# Return baseline first (preserves a sensible order), then extras
|
|
232
|
+
result = list(_BASELINE_MODELS)
|
|
233
|
+
for m in sorted(seen - set(_BASELINE_MODELS)):
|
|
234
|
+
result.append(m)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@router.get("/api/v1/ui/models", response_class=JSONResponse)
|
|
239
|
+
async def get_models() -> list[str]:
|
|
240
|
+
"""Return a sorted list of known model IDs for the model_select widget."""
|
|
241
|
+
return _collect_model_defaults()
|
hyperforge/trace.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import nucliadb_telemetry
|
|
5
|
+
import nucliadb_telemetry.metrics
|
|
6
|
+
from nucliadb_telemetry.utils import get_telemetry
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
|
|
9
|
+
SERVICE_NAME = os.environ.get("RAO_SERVICE_NAME", "hyperforge")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tracer(service_name: str = SERVICE_NAME):
|
|
13
|
+
provider = get_telemetry(service_name)
|
|
14
|
+
if provider:
|
|
15
|
+
return provider.get_tracer(__name__)
|
|
16
|
+
else:
|
|
17
|
+
return trace.NoOpTracer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
agent_observer = nucliadb_telemetry.metrics.Observer(
|
|
21
|
+
"hyperforge_agent", labels={"agent": ""}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Decorator to trace agent executions
|
|
26
|
+
def trace_agent(func):
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
async def wrapper(*args, **kwargs):
|
|
29
|
+
self = args[0]
|
|
30
|
+
agent_name = f"{self.__class__.__name__}.{func.__name__}"
|
|
31
|
+
try:
|
|
32
|
+
attributes = {"agent_id": self.config.id}
|
|
33
|
+
except:
|
|
34
|
+
attributes = {}
|
|
35
|
+
|
|
36
|
+
with (
|
|
37
|
+
tracer().start_as_current_span(agent_name, attributes=attributes),
|
|
38
|
+
agent_observer({"agent": agent_name}),
|
|
39
|
+
):
|
|
40
|
+
return await func(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import socket
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import aiodns
|
|
9
|
+
from nuclia_models.predict.generative_responses import GenerativeFullResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def iterate_tools_resp(
|
|
13
|
+
resp: GenerativeFullResponse,
|
|
14
|
+
) -> Iterable[Tuple[str, Dict[str, Any]]]:
|
|
15
|
+
if resp.tools is not None:
|
|
16
|
+
for _, tools_calls in resp.tools.items():
|
|
17
|
+
for tool in tools_calls:
|
|
18
|
+
if tool.function.name:
|
|
19
|
+
used_params = tool.function.arguments
|
|
20
|
+
yield tool.function.name, used_params
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sync_check_dns(url: str) -> Optional[str]:
|
|
24
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
25
|
+
error = None
|
|
26
|
+
try:
|
|
27
|
+
if parsed_url.hostname is None:
|
|
28
|
+
hostname = parsed_url.path
|
|
29
|
+
else:
|
|
30
|
+
hostname = parsed_url.hostname
|
|
31
|
+
addr = socket.gethostbyname_ex(hostname)
|
|
32
|
+
for addres in addr[2]:
|
|
33
|
+
if ipaddress.ip_address(addres).is_private:
|
|
34
|
+
error = "Its a private address"
|
|
35
|
+
except aiodns.error.DNSError:
|
|
36
|
+
error = "Could not find this URL"
|
|
37
|
+
return error
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sync_dns_validation(url: str) -> str:
|
|
41
|
+
error = sync_check_dns(url)
|
|
42
|
+
if error is not None:
|
|
43
|
+
raise ValueError(error)
|
|
44
|
+
return url
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def check_dns(url: str) -> str:
|
|
48
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
49
|
+
resolver = aiodns.DNSResolver()
|
|
50
|
+
error = None
|
|
51
|
+
try:
|
|
52
|
+
if parsed_url.hostname is None:
|
|
53
|
+
hostname = parsed_url.path
|
|
54
|
+
else:
|
|
55
|
+
hostname = parsed_url.hostname
|
|
56
|
+
addr = await resolver.gethostbyname(hostname, socket.AF_INET)
|
|
57
|
+
for addres in addr.addresses:
|
|
58
|
+
if ipaddress.ip_address(addres).is_private:
|
|
59
|
+
error = "Its a private address"
|
|
60
|
+
except aiodns.error.DNSError:
|
|
61
|
+
error = "Could not find this URL"
|
|
62
|
+
|
|
63
|
+
if error is not None:
|
|
64
|
+
raise ValueError(error)
|
|
65
|
+
return url
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WidgetType(str, Enum):
|
|
69
|
+
"""
|
|
70
|
+
Enumeration of available widget types for form field rendering.
|
|
71
|
+
These correspond to the frontend component mappings.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Model and AI-related widgets
|
|
75
|
+
MODEL_SELECT = "model_select"
|
|
76
|
+
"""Dropdown selector for AI models (LLM, embedding, etc.)"""
|
|
77
|
+
|
|
78
|
+
# Driver and source selection widgets
|
|
79
|
+
DRIVER_SELECT = "driver_select"
|
|
80
|
+
"""Selector for data source drivers (databases, APIs, etc.)"""
|
|
81
|
+
|
|
82
|
+
FILTERED_SOURCE_SELECT = "filtered_source_select"
|
|
83
|
+
"""Source selector with filtering capabilities based on transport type"""
|
|
84
|
+
|
|
85
|
+
# Code and text input widgets
|
|
86
|
+
CODE_EDITOR = "code_editor"
|
|
87
|
+
"""Code editor with syntax highlighting, supports multiple languages"""
|
|
88
|
+
|
|
89
|
+
EXPANDABLE_TEXTAREA = "expandable_textarea"
|
|
90
|
+
"""Multi-line text input that can expand/resize"""
|
|
91
|
+
|
|
92
|
+
# Specialized field widgets
|
|
93
|
+
TRANSPORT_FIELD = "transport_field"
|
|
94
|
+
"""Selector for transport/protocol types (HTTP, gRPC, etc.)"""
|
|
95
|
+
|
|
96
|
+
RULES_FIELD = "rules_field"
|
|
97
|
+
"""Complex rules configuration interface"""
|
|
98
|
+
|
|
99
|
+
KEY_VALUE_FIELD = "key_value_field"
|
|
100
|
+
"""Key-value pairs input (like environment variables)"""
|
|
101
|
+
|
|
102
|
+
# Basic form widgets
|
|
103
|
+
ARRAY_STRING_FIELD = "array_string_field"
|
|
104
|
+
"""Input for arrays of string values"""
|
|
105
|
+
|
|
106
|
+
ENUM_SELECT = "enum_select"
|
|
107
|
+
"""Dropdown for predefined enumeration values"""
|
|
108
|
+
|
|
109
|
+
# Extra for RAO
|
|
110
|
+
|
|
111
|
+
NOT_SHOWN = "not_show"
|
|
112
|
+
"""Field is not shown in the node config UI"""
|
hyperforge/utils/http.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import socket
|
|
3
|
+
|
|
4
|
+
import aiodns
|
|
5
|
+
from httpx import AsyncClient, AsyncHTTPTransport
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrivateUrlError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SafeTransport(AsyncHTTPTransport):
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
async def is_private_address(hostname: str) -> bool:
|
|
18
|
+
resolver = aiodns.DNSResolver()
|
|
19
|
+
addr = await resolver.gethostbyname(hostname, socket.AF_INET)
|
|
20
|
+
for address in addr.addresses:
|
|
21
|
+
if ipaddress.ip_address(address).is_private:
|
|
22
|
+
return True
|
|
23
|
+
if (
|
|
24
|
+
not ipaddress.ip_address(address).is_private
|
|
25
|
+
and not ipaddress.ip_address(address).is_global
|
|
26
|
+
):
|
|
27
|
+
# Matches shared address space (100.64.0.0/10 range) that should be non-routeable and
|
|
28
|
+
# not be used for public internet. Let's consider it internal
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
async def handle_async_request(self, request):
|
|
33
|
+
url = request.url
|
|
34
|
+
try:
|
|
35
|
+
hostname = url.host if url.host is not None else url.path
|
|
36
|
+
if await self.is_private_address(hostname):
|
|
37
|
+
raise PrivateUrlError("Cannot access private network resources")
|
|
38
|
+
except aiodns.error.DNSError:
|
|
39
|
+
raise PrivateUrlError("Could not lookup hostname")
|
|
40
|
+
|
|
41
|
+
return await super().handle_async_request(request)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def safe_http_client(timeout=30, transport_verify: bool = True):
|
|
45
|
+
return AsyncClient(
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
transport=SafeTransport(verify=transport_verify),
|
|
48
|
+
)
|
hyperforge/workflows.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from hyperforge.models import Rules
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkflowData(BaseModel):
|
|
10
|
+
model_config = ConfigDict(extra="ignore")
|
|
11
|
+
id: str
|
|
12
|
+
name: str
|
|
13
|
+
description: str | None
|
|
14
|
+
parameters: dict[str, Any] | None
|
|
15
|
+
rules: Rules | None = Field(default_factory=Rules)
|
|
16
|
+
required: list[str] = Field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkflowInput(BaseModel):
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
description: str | None = None
|
|
23
|
+
parameters: dict[str, Any] | None = None
|
|
24
|
+
rules: Rules = Field(default_factory=Rules)
|
|
25
|
+
required: list[str] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkflowUpdate(BaseModel):
|
|
29
|
+
name: str
|
|
30
|
+
description: str
|
|
31
|
+
parameters: dict[str, Any]
|
|
32
|
+
required: list[str] = Field(default_factory=list)
|
|
33
|
+
rules: Rules | None = Field(default_factory=Rules)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RetrievalAgent(BaseModel):
|
|
37
|
+
account: str
|
|
38
|
+
agent_id: str
|
|
39
|
+
memory: dict[str, Any] | None = None
|
|
40
|
+
description: str | None = None
|
|
41
|
+
title: str | None = None
|
|
42
|
+
instructions: str | None = None
|
|
43
|
+
created: datetime
|
|
44
|
+
modified: datetime | None = None
|