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