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
hyperforge/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ import logging
2
+
3
+ import fire # type: ignore
4
+ import jinja2
5
+
6
+ # Jinja vulnerability is not a concern since this is used to format a string prompt, not to render HTML
7
+ PROMPT_ENVIRONMENT = jinja2.Environment() # nosemgrep
8
+
9
+
10
+ logger = logging.getLogger("hyperforge")
11
+
12
+
13
+ def cli():
14
+ from hyperforge.arag import ARAG
15
+
16
+ fire.Fire(ARAG)
hyperforge/agent.py ADDED
@@ -0,0 +1,81 @@
1
+ import abc
2
+ import uuid
3
+ from typing import Any, Generic, List, Optional, Self, TypeVar
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from hyperforge.manager import Manager
8
+ from hyperforge.memory.memory import QuestionMemory
9
+ from hyperforge.utils import WidgetType
10
+
11
+
12
+ class AgentConfig(BaseModel):
13
+ id: Optional[str] = Field(default_factory=lambda: str(uuid.uuid4()))
14
+ title: str = "agent"
15
+ rules: Optional[List[str]] = Field(
16
+ default=None,
17
+ title="Agent rules",
18
+ description="List of rules to follow when executing this agent",
19
+ json_schema_extra={
20
+ "widget": WidgetType.NOT_SHOWN,
21
+ },
22
+ )
23
+ max_retries: int = 1
24
+ module: Any = Field(
25
+ ..., title="Agent module", description="Module/type of the agent"
26
+ )
27
+
28
+ def subagent_configure(self, config: dict):
29
+ pass
30
+
31
+
32
+ T_Config = TypeVar("T_Config", bound=AgentConfig)
33
+
34
+
35
+ class Agent(Generic[T_Config]):
36
+ __root_agent__: bool = False
37
+ agent_id: str
38
+ config: T_Config
39
+
40
+ def __init__(self, config: T_Config, agent_id: Optional[str] = None):
41
+ self.config: T_Config = config
42
+ self.agent_id: str = (
43
+ agent_id
44
+ if agent_id is not None
45
+ else config.id
46
+ if hasattr(config, "id") and config.id is not None
47
+ else str(uuid.uuid4())
48
+ )
49
+
50
+ @abc.abstractmethod
51
+ async def inner_from_config(self, config: T_Config, agent_id: Optional[str] = None):
52
+ """Initialize the agent from the config. This is where you should put any
53
+ async initialization code that needs to run when the agent is created.
54
+ """
55
+ pass
56
+
57
+ @classmethod
58
+ async def from_config(
59
+ cls, config: T_Config, agent_id: Optional[str] = None
60
+ ) -> Self:
61
+ instance = cls(config=config, agent_id=agent_id)
62
+ await instance.inner_from_config(config, agent_id)
63
+ return instance
64
+
65
+ def step_title(self, description: str) -> str:
66
+ """Format a step title as '<agent title>: <description>'.
67
+
68
+ Uses the user-configured instance title if set, otherwise falls back to
69
+ the Pydantic model_config title (e.g. 'MCP', 'SQL query') which describes
70
+ the agent type.
71
+ """
72
+ config = self.config # type: ignore[attr-defined]
73
+ title = type(config).model_config.get("title") or config.title # type: ignore[attr-defined]
74
+ return f"{title}: {description}"
75
+
76
+ async def __call__(
77
+ self,
78
+ memory: QuestionMemory,
79
+ manager: Manager,
80
+ ):
81
+ raise NotImplementedError()
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("hyperforge.api")
4
+
5
+ SERVICE_NAME = "hyperforge_api"
6
+
7
+
8
+ # Define the filter
9
+ class EndpointFilter(logging.Filter):
10
+ def filter(self, record: logging.LogRecord) -> bool:
11
+ return (
12
+ record.args is not None
13
+ and len(record.args) >= 3
14
+ and record.args[2] # type: ignore
15
+ not in ("/", "/metrics", "/health/alive", "/health/ready")
16
+ )
17
+
18
+
19
+ # Add filter to the logger
20
+ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
hyperforge/api/app.py ADDED
@@ -0,0 +1,155 @@
1
+ from typing import Any, Optional, Tuple
2
+
3
+ import prometheus_client # type: ignore
4
+ from fastapi import APIRouter, FastAPI
5
+ from lru import LRU
6
+ from mcp.server.lowlevel.server import Server as MCPServer
7
+ from mcp.server.streamable_http import (
8
+ StreamableHTTPServerTransport,
9
+ )
10
+ from nucliadb_sdk.v2.sdk import NucliaDBAsync
11
+ from nucliadb_telemetry.logs import setup_logging
12
+ from nucliadb_telemetry.settings import LogLevel, LogSettings
13
+ from nucliadb_telemetry.utils import clean_telemetry, setup_telemetry
14
+ from prometheus_client import CONTENT_TYPE_LATEST # type: ignore
15
+ from starlette.middleware.authentication import AuthenticationMiddleware
16
+ from starlette.responses import PlainTextResponse
17
+
18
+ from hyperforge.api import SERVICE_NAME, internal, logger, v1
19
+ from hyperforge.api.authentication import RaoAuthenticationBackend
20
+ from hyperforge.api.logging import set_sentry
21
+ from hyperforge.api.settings import Settings
22
+ from hyperforge.broker import Broker
23
+ from hyperforge.broker.redis import RedisBroker
24
+ from hyperforge.configure import GLOBAL_REGISTRY, load_all_configurations, scan
25
+ from hyperforge.db.agents import AgentManager
26
+ from hyperforge.db.settings import DataManagerSettings
27
+ from hyperforge.feature_flag import get_flag_service
28
+
29
+ router = APIRouter()
30
+
31
+
32
+ @router.get("/metrics")
33
+ async def serve_metrics(): # pragma: no cover
34
+ output = prometheus_client.exposition.generate_latest()
35
+ return PlainTextResponse(
36
+ output.decode("utf8"), headers={"Content-Type": CONTENT_TYPE_LATEST}
37
+ )
38
+
39
+
40
+ @router.get("/health/ready")
41
+ async def health_ready():
42
+ return {"status": "ok"}
43
+
44
+
45
+ @router.get("/health/alive")
46
+ async def health_alive():
47
+ return {"status": "ok"}
48
+
49
+
50
+ class HTTPApplication(FastAPI):
51
+ agent_manager: AgentManager
52
+ arag_search: NucliaDBAsync
53
+ arag_writer: NucliaDBAsync
54
+ arag_reader: NucliaDBAsync
55
+ broker: Broker
56
+ extra_middlewares: Optional[list[Any]] = None
57
+
58
+ def __init__(
59
+ self,
60
+ settings: Settings,
61
+ data_manager_settings: DataManagerSettings,
62
+ *args,
63
+ **kwargs,
64
+ ):
65
+ super().__init__(*args, **kwargs)
66
+ self.settings = settings
67
+ self.data_manager_settings = data_manager_settings
68
+ self.include_router(internal.router)
69
+ self.include_router(v1.router)
70
+ self.include_router(router)
71
+ self.add_middleware(
72
+ AuthenticationMiddleware,
73
+ backend=RaoAuthenticationBackend(),
74
+ )
75
+ if self.extra_middlewares is not None:
76
+ for extra_middleware in self.extra_middlewares:
77
+ self.add_middleware(extra_middleware)
78
+ self.add_event_handler("startup", self.startup)
79
+ self.add_event_handler("shutdown", self.shutdown)
80
+
81
+ async def startup(self) -> None:
82
+ GLOBAL_REGISTRY.clear()
83
+ await setup_telemetry(SERVICE_NAME)
84
+ setup_logging(
85
+ settings=LogSettings(
86
+ debug=self.settings.debug,
87
+ log_level=LogLevel(self.settings.log_level),
88
+ logger_levels={
89
+ "uvicorn.error": LogLevel.ERROR,
90
+ "nucliadb_telemetry": LogLevel.ERROR,
91
+ "mcp.client.streamable_http": LogLevel.WARNING,
92
+ "mcp.server.lowlevel.server": LogLevel.WARNING,
93
+ "hyperforge.configure": LogLevel.WARNING,
94
+ },
95
+ )
96
+ )
97
+ if self.settings.sentry_url is not None:
98
+ set_sentry(
99
+ self.settings.zone,
100
+ self.settings.running_environment,
101
+ self.settings.sentry_url,
102
+ )
103
+
104
+ get_flag_service() # precache the flag service
105
+
106
+ if self.settings.memory_apikey_nucliadb is None:
107
+ api_key = None
108
+ headers = {"X-NUCLIADB-ROLES": "WRITER;READER"}
109
+ else:
110
+ api_key = self.settings.memory_apikey_nucliadb
111
+ headers = None
112
+
113
+ self.arag_writer = NucliaDBAsync(
114
+ url=self.settings.memory_writer_nucliadb,
115
+ api_key=api_key,
116
+ headers=headers,
117
+ )
118
+ self.arag_reader = NucliaDBAsync(
119
+ url=self.settings.memory_reader_nucliadb,
120
+ api_key=api_key,
121
+ headers=headers,
122
+ )
123
+ self.arag_search = NucliaDBAsync(
124
+ url=self.settings.memory_search_nucliadb,
125
+ api_key=api_key,
126
+ headers=headers,
127
+ )
128
+
129
+ self.broker = RedisBroker.from_url(
130
+ url=self.settings.valkey_url,
131
+ activate_subject=self.settings.activate_subject,
132
+ keepalive_ms=int(self.settings.pubsub_keepalive_seconds * 1000),
133
+ cluster_mode=self.settings.valkey_cluster_mode,
134
+ )
135
+
136
+ self.sses: LRU[Tuple[str, str], StreamableHTTPServerTransport] = LRU(size=100)
137
+ self.mcp_servers: LRU[str, MCPServer] = LRU(size=100)
138
+
139
+ self.agent_manager = await AgentManager.from_settings(
140
+ settings=self.data_manager_settings
141
+ )
142
+ await self.agent_manager.initialize()
143
+
144
+ for load_module in self.settings.load_modules:
145
+ try:
146
+ scan(load_module)
147
+ load_all_configurations(load_module)
148
+ except ImportError:
149
+ logger.error(f"Module {load_module} could not be loaded")
150
+
151
+ async def shutdown(self) -> None:
152
+ await self.agent_manager.finalize()
153
+ await self.broker.finalize()
154
+ await clean_telemetry(SERVICE_NAME)
155
+ GLOBAL_REGISTRY.clear()
@@ -0,0 +1,271 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ import typing
5
+ from enum import Enum
6
+ from typing import Optional
7
+
8
+ from starlette.authentication import AuthCredentials, AuthenticationBackend, BaseUser
9
+ from starlette.exceptions import HTTPException
10
+ from starlette.requests import HTTPConnection, Request
11
+ from starlette.responses import RedirectResponse, Response
12
+ from starlette.websockets import WebSocket
13
+
14
+
15
+ class User(BaseUser):
16
+ def __init__(self, username: str, security_groups: list[str] | None = None) -> None:
17
+ self.username = username
18
+ self._security_groups = security_groups
19
+
20
+ @property
21
+ def is_authenticated(self) -> bool:
22
+ return True
23
+
24
+ @property
25
+ def display_name(self) -> str:
26
+ return self.username
27
+
28
+ @property
29
+ def security_groups(self) -> list[str] | None:
30
+ return self._security_groups
31
+
32
+
33
+ class RaoAuthenticationBackend(AuthenticationBackend):
34
+ """Authentication backend with a mixture of RAO and NucliaDB auth.
35
+
36
+ This mixture is required while migrating /ask endpoint from NucliaDB to RAO,
37
+ as roles are injected by authorizer and it resolves by path instead of path
38
+ and service. Thus, we handle NucliaDB auth headers (X-NUCLIADB-*) as well as
39
+ the regular learning headers (X-STF-*)
40
+
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ self.roles_headers = [
45
+ "X-STF-ROLES",
46
+ "X-NUCLIADB-ROLES",
47
+ ]
48
+ self.user_headers = [
49
+ "X-STF-USER",
50
+ "X-NUCLIADB-USER",
51
+ ]
52
+ self.security_groups_headers = ["X-NUCLIADB-SECURITY-GROUPS"]
53
+
54
+ async def authenticate(self, request) -> tuple[AuthCredentials, BaseUser] | None:
55
+ # There are two groups of headers to authenticate: X-STF-* and
56
+ # X-NUCLIADB-*. As authorizer should only resolve to one set of headers,
57
+ # we scan and try to find any of both. While endpoint roles are properly
58
+ # synchronized with authorizer rules, we don't really care which one
59
+ # there is, nor we will mix them
60
+
61
+ auth_creds = None
62
+ for roles_header in self.roles_headers:
63
+ if roles_header in request.headers:
64
+ header_roles = request.headers[roles_header]
65
+ roles = header_roles.split(";")
66
+ auth_creds = AuthCredentials(roles)
67
+ break
68
+
69
+ if auth_creds is None:
70
+ return None
71
+
72
+ user = None
73
+ for user_header in self.user_headers:
74
+ if user_header in request.headers:
75
+ user = request.headers[user_header]
76
+
77
+ raw_security_groups: str | None = None
78
+ for security_group_header in self.security_groups_headers:
79
+ if security_group_header in request.headers:
80
+ raw_security_groups = request.headers[security_group_header]
81
+ break
82
+
83
+ security_groups: list[str] | None = None
84
+ if raw_security_groups is not None:
85
+ security_groups = raw_security_groups.split(";")
86
+
87
+ user = User(username=user, security_groups=security_groups)
88
+ break
89
+
90
+ if user is None:
91
+ user = User(username="Anonymous")
92
+
93
+ return auth_creds, user
94
+
95
+
96
+ def has_required_scope(conn: HTTPConnection, scopes: typing.Sequence[str]) -> bool:
97
+ if conn.auth is None or conn.auth.scopes is None:
98
+ raise HTTPException(status_code=403, detail="Missing authorizer headers.")
99
+
100
+ for scope in scopes:
101
+ if scope in conn.auth.scopes:
102
+ return True
103
+ return False
104
+
105
+
106
+ def requires(
107
+ scopes: typing.Union[str, typing.Sequence[str]],
108
+ status_code: int = 403,
109
+ redirect: Optional[str] = None,
110
+ ) -> typing.Callable:
111
+ # As a fastapi requirement, custom Enum classes have to inherit also from
112
+ # string, so we MUST check for Enum before str
113
+ if isinstance(scopes, Enum):
114
+ scopes_list = [scopes.value]
115
+ elif isinstance(scopes, str):
116
+ scopes_list = [scopes]
117
+ elif isinstance(scopes, list):
118
+ scopes_list = [
119
+ scope.value if isinstance(scope, Enum) else scope for scope in scopes
120
+ ]
121
+
122
+ def decorator(func: typing.Callable) -> typing.Callable:
123
+ func.__required_scopes__ = scopes_list # type: ignore
124
+ type = None
125
+ sig = inspect.signature(func)
126
+ for idx, parameter in enumerate(sig.parameters.values()):
127
+ if parameter.name == "request" or parameter.name == "websocket":
128
+ type = parameter.name
129
+ break
130
+ else:
131
+ raise Exception(
132
+ f'No "request" or "websocket" argument on function "{func}"'
133
+ )
134
+
135
+ if type == "websocket":
136
+ # Handle websocket functions. (Always async)
137
+ @functools.wraps(func)
138
+ async def websocket_wrapper(
139
+ *args: typing.Any, **kwargs: typing.Any
140
+ ) -> None:
141
+ websocket = kwargs.get("websocket", None)
142
+ assert isinstance(websocket, WebSocket)
143
+
144
+ if not has_required_scope(websocket, scopes_list):
145
+ await websocket.close()
146
+ else:
147
+ await func(*args, **kwargs)
148
+
149
+ return websocket_wrapper
150
+
151
+ elif asyncio.iscoroutinefunction(func):
152
+ # Handle async request/response functions.
153
+ @functools.wraps(func)
154
+ async def async_wrapper(
155
+ *args: typing.Any, **kwargs: typing.Any
156
+ ) -> Response:
157
+ request = kwargs.get("request", None)
158
+ assert isinstance(request, Request)
159
+
160
+ if not has_required_scope(request, scopes_list):
161
+ if redirect is not None:
162
+ return RedirectResponse(
163
+ url=request.url_for(redirect), status_code=303
164
+ )
165
+ raise HTTPException(status_code=status_code)
166
+ return await func(*args, **kwargs)
167
+
168
+ return async_wrapper
169
+
170
+ else:
171
+ # Handle sync request/response functions.
172
+ @functools.wraps(func)
173
+ def sync_wrapper(*args: typing.Any, **kwargs: typing.Any) -> Response:
174
+ request = kwargs.get("request", args[idx])
175
+ assert isinstance(request, Request)
176
+
177
+ if not has_required_scope(request, scopes_list):
178
+ if redirect is not None:
179
+ return RedirectResponse(
180
+ url=request.url_for(redirect), status_code=303
181
+ )
182
+ raise HTTPException(status_code=status_code)
183
+ return func(*args, **kwargs)
184
+
185
+ return sync_wrapper
186
+
187
+ return decorator
188
+
189
+
190
+ def requires_one(
191
+ scopes: typing.Union[str, typing.Sequence[str]],
192
+ status_code: int = 403,
193
+ redirect: Optional[str] = None,
194
+ ) -> typing.Callable:
195
+ # As a fastapi requirement, custom Enum classes have to inherit also from
196
+ # string, so we MUST check for Enum before str
197
+ if isinstance(scopes, Enum):
198
+ scopes_list = [scopes.value]
199
+ elif isinstance(scopes, str):
200
+ scopes_list = [scopes]
201
+ elif isinstance(scopes, list):
202
+ scopes_list = [
203
+ scope.value if isinstance(scope, Enum) else scope for scope in scopes
204
+ ]
205
+
206
+ def decorator(func: typing.Callable) -> typing.Callable:
207
+ func.__required_scopes__ = scopes_list # type: ignore
208
+ type = None
209
+ sig = inspect.signature(func)
210
+ for idx, parameter in enumerate(sig.parameters.values()):
211
+ if parameter.name == "request" or parameter.name == "websocket":
212
+ type = parameter.name
213
+ break
214
+ else:
215
+ raise Exception(
216
+ f'No "request" or "websocket" argument on function "{func}"'
217
+ )
218
+
219
+ if type == "websocket":
220
+ # Handle websocket functions. (Always async)
221
+ @functools.wraps(func)
222
+ async def websocket_wrapper(
223
+ *args: typing.Any, **kwargs: typing.Any
224
+ ) -> None:
225
+ websocket = kwargs.get("websocket", None)
226
+ assert isinstance(websocket, WebSocket)
227
+
228
+ if not has_required_scope(websocket, scopes_list):
229
+ await websocket.close()
230
+ else:
231
+ await func(*args, **kwargs)
232
+
233
+ return websocket_wrapper
234
+
235
+ elif asyncio.iscoroutinefunction(func):
236
+ # Handle async request/response functions.
237
+ @functools.wraps(func)
238
+ async def async_wrapper(
239
+ *args: typing.Any, **kwargs: typing.Any
240
+ ) -> Response:
241
+ request = kwargs.get("request", None)
242
+ assert isinstance(request, Request)
243
+
244
+ if not has_required_scope(request, scopes_list):
245
+ if redirect is not None:
246
+ return RedirectResponse(
247
+ url=request.url_for(redirect), status_code=303
248
+ )
249
+ raise HTTPException(status_code=status_code)
250
+ return await func(*args, **kwargs)
251
+
252
+ return async_wrapper
253
+
254
+ else:
255
+ # Handle sync request/response functions.
256
+ @functools.wraps(func)
257
+ def sync_wrapper(*args: typing.Any, **kwargs: typing.Any) -> Response:
258
+ request = kwargs.get("request", args[idx])
259
+ assert isinstance(request, Request)
260
+
261
+ if not has_required_scope(request, scopes_list):
262
+ if redirect is not None:
263
+ return RedirectResponse(
264
+ url=request.url_for(redirect), status_code=303
265
+ )
266
+ raise HTTPException(status_code=status_code)
267
+ return func(*args, **kwargs)
268
+
269
+ return sync_wrapper
270
+
271
+ return decorator
@@ -0,0 +1,33 @@
1
+ import uvicorn
2
+ from nucliadb_telemetry.fastapi import instrument_app
3
+ from nucliadb_telemetry.logs import setup_logging
4
+ from nucliadb_telemetry.utils import get_telemetry
5
+
6
+ from hyperforge import openapi
7
+ from hyperforge.api import SERVICE_NAME
8
+ from hyperforge.api.app import HTTPApplication
9
+ from hyperforge.api.settings import Settings
10
+ from hyperforge.api.v1.router import router
11
+ from hyperforge.db.settings import DataManagerSettings
12
+
13
+
14
+ def run(): # pragma: no cover
15
+ setup_logging()
16
+ settings = Settings()
17
+ data_manager_settings = DataManagerSettings()
18
+ app = HTTPApplication(
19
+ settings,
20
+ data_manager_settings=data_manager_settings,
21
+ )
22
+ instrument_app(
23
+ app,
24
+ tracer_provider=get_telemetry(SERVICE_NAME),
25
+ excluded_urls=["/", "/metrics", "/health/ready", "/health/alive"],
26
+ metrics=True,
27
+ trace_id_on_responses=True,
28
+ )
29
+ uvicorn.run(app, host=settings.http_host, port=settings.http_port)
30
+
31
+
32
+ def extract_openapi():
33
+ openapi.extract_openapi_command("arag", "ARAG API", router)
@@ -0,0 +1,4 @@
1
+ from . import inspect
2
+ from .router import router
3
+
4
+ __all__ = ["inspect", "router"]
@@ -0,0 +1,30 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from starlette.requests import Request
4
+
5
+ from hyperforge.api.internal.router import router
6
+ from hyperforge.api.models import InspectData
7
+ from hyperforge.db.agents import AgentManager
8
+
9
+ if TYPE_CHECKING:
10
+ from hyperforge.api.app import HTTPApplication
11
+
12
+
13
+ @router.get(
14
+ "/api/internal/v1/agent/{kbid}",
15
+ status_code=200,
16
+ description="Report task is done",
17
+ tags=["Task"],
18
+ include_in_schema=False,
19
+ )
20
+ async def inspect_agent_info(request: Request, kbid: str, account: str) -> InspectData:
21
+ app: HTTPApplication = request.app
22
+ agent_manager: AgentManager = app.agent_manager
23
+
24
+ return InspectData(
25
+ contexts=await agent_manager.get_context(account, kbid),
26
+ driver=await agent_manager.get_drivers(account, kbid),
27
+ postprocess=await agent_manager.get_postprocess(account, kbid),
28
+ preprocess=await agent_manager.get_preprocess(account, kbid),
29
+ rules=await agent_manager.get_rules(account, kbid),
30
+ )
@@ -0,0 +1,3 @@
1
+ from fastapi.routing import APIRouter
2
+
3
+ router = APIRouter()
@@ -0,0 +1,18 @@
1
+ from importlib.metadata import version
2
+ from typing import Optional
3
+
4
+ import sentry_sdk
5
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
6
+
7
+
8
+ def set_sentry(zone: str, environment: str, sentry_url: Optional[str] = None):
9
+ if sentry_url:
10
+ sentry_exception = ExcepthookIntegration(always_run=True)
11
+ version_num = version("hyperforge")
12
+ sentry_sdk.init(
13
+ release=version_num,
14
+ environment=environment,
15
+ dsn=sentry_url,
16
+ integrations=[sentry_exception],
17
+ )
18
+ sentry_sdk.set_tag("zone", zone)