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,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"""
@@ -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
+ )
@@ -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