hyperforge 1.0.0.post55__tar.gz → 1.0.0.post57__tar.gz
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-1.0.0.post55 → hyperforge-1.0.0.post57}/PKG-INFO +1 -1
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/pyproject.toml +1 -1
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/models.py +5 -1
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/interaction.py +1 -0
- hyperforge-1.0.0.post57/src/hyperforge/api/v1/oauth.py +163 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/engine.py +6 -2
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/interaction.py +1 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/memory/memory.py +54 -25
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/pubsub.py +6 -1
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/session.py +6 -1
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/utils.py +2 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/settings.py +6 -0
- hyperforge-1.0.0.post55/src/hyperforge/api/v1/oauth.py +0 -60
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/README.md +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/VERSION +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/agent.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/README +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/env.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/script.py.mako +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/049a4aed19e1_create_generation_table.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/1416cb41bd49_add_downloads_table.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/2847934e2d59_unique_driver.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/4d89a36c8bda_memory.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/72b997edb05d_identifier_for_drivers.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/9c6f6a1b4e7f_workflows_default_fk.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/b8edb72295e0_.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/f3d2c1b0a9e8_soft_delete_workflows.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic.ini +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/app.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/authentication.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/commands.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/inspect.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/router.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/logging.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/session.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/settings.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/utils.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/agents.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/mcp_content.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/mcp_interaction.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/prompt.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/router.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/schema.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/session.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/utils.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/workflows.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/arag.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/local.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/redis.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/configure.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/agent.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/config.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/database.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/agents.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/encryption.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/exceptions.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/settings.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/workflow_cleanup.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/definition.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/driver.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/dummy.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/exceptions.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/feature_flag.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/fixtures.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/llm.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/manager.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/memory/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/minimal_fixtures.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/models.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/nua.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/openapi.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/prompts.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/py.typed +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/redis_utils.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/agent.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/config.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/cache.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/run.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/sandbox.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/settings.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/web.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/agent.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/app.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/config.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/const.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/run.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/settings.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/ui_router.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-DLejS5jC.js +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-MoxkZ1_3.css +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/favicon.svg +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/index.html +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/trace.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/utils/__init__.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/utils/http.py +0 -0
- {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/workflows.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hyperforge"
|
|
7
|
-
version = "1.0.0.
|
|
7
|
+
version = "1.0.0.post57"
|
|
8
8
|
authors = [{ name = "AI Data Team", email = "learning@nuclia.com" }]
|
|
9
9
|
description = "Agentic Framework for Orchestrated Runtime, Governance, and Execution"
|
|
10
10
|
readme = "README.md"
|
|
@@ -5,7 +5,7 @@ from nucliadb_models import TextFormat
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
from hyperforge.driver import DriverConfig
|
|
8
|
-
from hyperforge.models import Rules
|
|
8
|
+
from hyperforge.models import HistoryQuestionAnswer, Rules
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class StashRoles(str, Enum):
|
|
@@ -125,5 +125,9 @@ class InteractionRequest(BaseModel):
|
|
|
125
125
|
question: str
|
|
126
126
|
headers: Dict[str, str] = {}
|
|
127
127
|
arguments: Dict[str, str] = {}
|
|
128
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = Field(
|
|
129
|
+
default=None,
|
|
130
|
+
description="Client-managed chat history. When set (even to an empty list), overrides any server-side session history for agents that use previous Q&A context (rephrase, summarize, smart, etc.). Omit the field entirely to use server-side session history.",
|
|
131
|
+
)
|
|
128
132
|
operation: InteractionOperation = InteractionOperation.QUESTION
|
|
129
133
|
streaming: bool = False
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
|
+
|
|
4
|
+
from cryptography.fernet import InvalidToken
|
|
5
|
+
from fastapi import Query
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from starlette.responses import HTMLResponse
|
|
8
|
+
|
|
9
|
+
from hyperforge.api.settings import Settings
|
|
10
|
+
from hyperforge.api.v1.router import router
|
|
11
|
+
from hyperforge.api.v1.utils import tracer
|
|
12
|
+
from hyperforge_mcp.http import _fingerprint, decrypt_mcp_oauth_state
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
RENDER = "<html><body><h1>OAuth Completed</h1><p>You can close this window and return to the application.</p></body></html>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_oauth_subject(
|
|
20
|
+
settings: Settings,
|
|
21
|
+
account_id: str,
|
|
22
|
+
agent_id: str,
|
|
23
|
+
workflow_id: str,
|
|
24
|
+
session: str,
|
|
25
|
+
question_id: str,
|
|
26
|
+
oauth_uuid: str,
|
|
27
|
+
) -> str:
|
|
28
|
+
return settings.oauth_subject.format(
|
|
29
|
+
account=account_id,
|
|
30
|
+
agent_id=agent_id,
|
|
31
|
+
session=session,
|
|
32
|
+
question=question_id,
|
|
33
|
+
oauth_uuid=oauth_uuid,
|
|
34
|
+
workflow_id=workflow_id,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get(
|
|
39
|
+
"/api/auth/agent/{agent_id}/workflow/{workflow_id}/session/{session}/oauth/{oauth_uuid}/callback",
|
|
40
|
+
status_code=200,
|
|
41
|
+
description="Get Agent Schema",
|
|
42
|
+
tags=["Retrieval Agent"],
|
|
43
|
+
include_in_schema=False,
|
|
44
|
+
)
|
|
45
|
+
async def oauth_callback(
|
|
46
|
+
request: Request,
|
|
47
|
+
agent_id: str,
|
|
48
|
+
session: str,
|
|
49
|
+
workflow_id: str,
|
|
50
|
+
oauth_uuid: str,
|
|
51
|
+
question_id: str = Query(..., include_in_schema=False),
|
|
52
|
+
state: str = Query(..., include_in_schema=False),
|
|
53
|
+
account_id: str = Query(..., include_in_schema=False),
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Callback from oauth flow on RAO that requires to send creds to websocket
|
|
57
|
+
"""
|
|
58
|
+
settings: Settings = request.app.settings
|
|
59
|
+
subject = _build_oauth_subject(
|
|
60
|
+
settings,
|
|
61
|
+
account_id,
|
|
62
|
+
agent_id,
|
|
63
|
+
workflow_id,
|
|
64
|
+
session,
|
|
65
|
+
question_id,
|
|
66
|
+
oauth_uuid,
|
|
67
|
+
)
|
|
68
|
+
# Request a question
|
|
69
|
+
with tracer().start_as_current_span("Request activation"):
|
|
70
|
+
logger.info(
|
|
71
|
+
"OAuth callback received for agent=%s, session=%s, oauth_uuid=%s, question_id=%s",
|
|
72
|
+
agent_id,
|
|
73
|
+
session,
|
|
74
|
+
oauth_uuid,
|
|
75
|
+
question_id,
|
|
76
|
+
)
|
|
77
|
+
await request.app.broker.send_reply(subject, state)
|
|
78
|
+
logger.info(
|
|
79
|
+
"OAuth callback published to stream %s",
|
|
80
|
+
subject,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return HTMLResponse(content=RENDER)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get(
|
|
87
|
+
"/api/auth/mcp/callback",
|
|
88
|
+
status_code=200,
|
|
89
|
+
description="Generic MCP OAuth callback (fixed redirect URI, state-routed)",
|
|
90
|
+
tags=["Retrieval Agent"],
|
|
91
|
+
include_in_schema=False,
|
|
92
|
+
)
|
|
93
|
+
async def mcp_oauth_callback_generic(
|
|
94
|
+
request: Request,
|
|
95
|
+
code: str | None = Query(None, include_in_schema=False),
|
|
96
|
+
state: str | None = Query(None, include_in_schema=False),
|
|
97
|
+
error: str | None = Query(None, include_in_schema=False),
|
|
98
|
+
error_description: str | None = Query(None, include_in_schema=False),
|
|
99
|
+
):
|
|
100
|
+
settings: Settings = request.app.settings
|
|
101
|
+
|
|
102
|
+
if state is None:
|
|
103
|
+
logger.warning("MCP generic OAuth callback received without state parameter")
|
|
104
|
+
return HTMLResponse(content="Missing OAuth state parameter", status_code=400)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
routing = decrypt_mcp_oauth_state(state)
|
|
108
|
+
except InvalidToken:
|
|
109
|
+
logger.warning(
|
|
110
|
+
"MCP generic OAuth callback: invalid or expired state (Fernet decryption failed)"
|
|
111
|
+
)
|
|
112
|
+
return HTMLResponse(content="Invalid or expired OAuth state", status_code=400)
|
|
113
|
+
|
|
114
|
+
sdk_state = routing.sdk_state or None
|
|
115
|
+
if sdk_state is None:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"MCP generic OAuth callback: decrypted state missing sdk_state field"
|
|
118
|
+
)
|
|
119
|
+
return HTMLResponse(content="Malformed OAuth state", status_code=400)
|
|
120
|
+
|
|
121
|
+
subject = _build_oauth_subject(
|
|
122
|
+
settings,
|
|
123
|
+
routing.account_id,
|
|
124
|
+
routing.agent_id,
|
|
125
|
+
routing.workflow_id,
|
|
126
|
+
routing.session_id,
|
|
127
|
+
routing.question_id,
|
|
128
|
+
routing.oauth_uuid,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
payload_data: dict[str, str] = {}
|
|
132
|
+
if code is not None:
|
|
133
|
+
payload_data["code"] = code
|
|
134
|
+
if sdk_state is not None:
|
|
135
|
+
payload_data["state"] = sdk_state
|
|
136
|
+
if error is not None:
|
|
137
|
+
payload_data["error"] = error
|
|
138
|
+
if error_description is not None:
|
|
139
|
+
payload_data["error_description"] = error_description
|
|
140
|
+
|
|
141
|
+
payload = urlencode(payload_data)
|
|
142
|
+
|
|
143
|
+
with tracer().start_as_current_span("MCP generic OAuth callback"):
|
|
144
|
+
logger.info(
|
|
145
|
+
"MCP generic OAuth callback: agent=%s workflow=%s session=%s question_id=%s oauth_uuid=%s sdk_state_fp=%s has_code=%s has_error=%s",
|
|
146
|
+
routing.agent_id,
|
|
147
|
+
routing.workflow_id,
|
|
148
|
+
routing.session_id,
|
|
149
|
+
routing.question_id,
|
|
150
|
+
routing.oauth_uuid,
|
|
151
|
+
_fingerprint(sdk_state),
|
|
152
|
+
code is not None,
|
|
153
|
+
error is not None,
|
|
154
|
+
)
|
|
155
|
+
await request.app.broker.send_reply(subject, payload)
|
|
156
|
+
logger.info("mcp_oauth send_reply: published to stream %s", subject)
|
|
157
|
+
|
|
158
|
+
if error is not None:
|
|
159
|
+
desc = f": {error_description}" if error_description else ""
|
|
160
|
+
return HTMLResponse(
|
|
161
|
+
content=f"OAuth authorization failed ({error}{desc})", status_code=400
|
|
162
|
+
)
|
|
163
|
+
return HTMLResponse(content=RENDER)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, cast
|
|
3
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, cast
|
|
4
4
|
|
|
5
5
|
from nuclia.lib.nua import AsyncNuaClient
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ from hyperforge.interaction import AragAnswer
|
|
|
9
9
|
from hyperforge.llm import NoopNuaClient, NuaBaseModel, NUAConnection
|
|
10
10
|
from hyperforge.manager import Manager
|
|
11
11
|
from hyperforge.memory.memory import BaseSessionMemory, QuestionMemory, SessionMemory
|
|
12
|
+
from hyperforge.models import HistoryQuestionAnswer
|
|
12
13
|
from hyperforge.retrieval.agent import RetrievalAgent
|
|
13
14
|
from hyperforge.retrieval.config import RetrievalAgentConfig
|
|
14
15
|
|
|
@@ -84,6 +85,7 @@ async def main(
|
|
|
84
85
|
headers: Optional[Dict[str, str]] = None,
|
|
85
86
|
memory_klass: type[BaseSessionMemory] = SessionMemory,
|
|
86
87
|
streaming: bool = False,
|
|
88
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = None,
|
|
87
89
|
) -> QuestionMemory:
|
|
88
90
|
try:
|
|
89
91
|
state, session_memory = await init(
|
|
@@ -99,7 +101,9 @@ async def main(
|
|
|
99
101
|
session_id=session_id,
|
|
100
102
|
memory_klass=memory_klass,
|
|
101
103
|
)
|
|
102
|
-
question_memory = session_memory.start_question(
|
|
104
|
+
question_memory = session_memory.start_question(
|
|
105
|
+
question, streaming=streaming, chat_history=chat_history
|
|
106
|
+
)
|
|
103
107
|
if callback is not None:
|
|
104
108
|
question_memory.set_callback_fn(callback)
|
|
105
109
|
|
|
@@ -54,6 +54,26 @@ logger = logging.getLogger("arag.memory")
|
|
|
54
54
|
|
|
55
55
|
# NucliaDB storage
|
|
56
56
|
QUESTION_ANSWERS_FIELD: str = "qas"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _qa_list_to_context_string(history: List[HistoryQuestionAnswer]) -> Tuple[str, int]:
|
|
60
|
+
"""Format a list of Q&A pairs into the prompt context string used by agents."""
|
|
61
|
+
result = "".join(
|
|
62
|
+
f"- Question: {qa.question}\n- Answer: {qa.answer}\n" for qa in history
|
|
63
|
+
)
|
|
64
|
+
return result, len(history)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _qa_list_to_chat_messages(history: List[HistoryQuestionAnswer]) -> List[Message]:
|
|
68
|
+
"""Convert a list of Q&A pairs into the alternating User/Nuclia Message list used by LLMs."""
|
|
69
|
+
return [
|
|
70
|
+
msg
|
|
71
|
+
for qa in history
|
|
72
|
+
for msg in (
|
|
73
|
+
Message(author=Author.USER, text=qa.question),
|
|
74
|
+
Message(author=Author.NUCLIA, text=qa.answer),
|
|
75
|
+
)
|
|
76
|
+
]
|
|
57
77
|
CONTEXT_FIELD: str = "context"
|
|
58
78
|
STEPS_FIELD: str = "steps"
|
|
59
79
|
USER_INFO_FIELD: str = "user_info"
|
|
@@ -65,6 +85,7 @@ class BaseSessionMemory:
|
|
|
65
85
|
|
|
66
86
|
agent_id: str = ""
|
|
67
87
|
workflow_id: str = ""
|
|
88
|
+
account_id: str = ""
|
|
68
89
|
kbid: Optional[str] = None
|
|
69
90
|
|
|
70
91
|
# User information dictionary
|
|
@@ -112,34 +133,13 @@ class BaseSessionMemory:
|
|
|
112
133
|
return KnowledgeboxFindResults(total=0, resources={})
|
|
113
134
|
|
|
114
135
|
async def get_chat_history(self) -> List[Message]:
|
|
115
|
-
|
|
116
|
-
result = []
|
|
117
|
-
for qa in qas:
|
|
118
|
-
result.append(
|
|
119
|
-
Message(
|
|
120
|
-
author=Author.USER,
|
|
121
|
-
text=qa.question,
|
|
122
|
-
)
|
|
123
|
-
)
|
|
124
|
-
result.append(
|
|
125
|
-
Message(
|
|
126
|
-
author=Author.NUCLIA,
|
|
127
|
-
text=qa.answer,
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
return result
|
|
136
|
+
return _qa_list_to_chat_messages(await self.qa_history())
|
|
131
137
|
|
|
132
138
|
async def qa_history(self) -> list[HistoryQuestionAnswer]:
|
|
133
139
|
return []
|
|
134
140
|
|
|
135
141
|
async def context_history(self) -> Tuple[str, int]:
|
|
136
|
-
|
|
137
|
-
interactions = 0
|
|
138
|
-
for qa in await self.qa_history():
|
|
139
|
-
result += f"- Question: {qa.question}\n"
|
|
140
|
-
result += f"- Answer: {qa.answer}\n"
|
|
141
|
-
interactions += 1
|
|
142
|
-
return result, interactions
|
|
142
|
+
return _qa_list_to_context_string(await self.qa_history())
|
|
143
143
|
|
|
144
144
|
def start_question(
|
|
145
145
|
self,
|
|
@@ -149,6 +149,7 @@ class BaseSessionMemory:
|
|
|
149
149
|
headers: Dict[str, str] = {},
|
|
150
150
|
arguments: Dict[str, str] = {},
|
|
151
151
|
streaming: bool = False,
|
|
152
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = None,
|
|
152
153
|
) -> "QuestionMemory":
|
|
153
154
|
return QuestionMemory(
|
|
154
155
|
self,
|
|
@@ -158,6 +159,7 @@ class BaseSessionMemory:
|
|
|
158
159
|
headers=headers,
|
|
159
160
|
arguments=arguments,
|
|
160
161
|
streaming=streaming,
|
|
162
|
+
chat_history=chat_history,
|
|
161
163
|
)
|
|
162
164
|
|
|
163
165
|
async def save(self, question: "QuestionMemory") -> None:
|
|
@@ -183,6 +185,7 @@ class NoMemorySessionMemory(BaseSessionMemory):
|
|
|
183
185
|
headers: Dict[str, str] = {},
|
|
184
186
|
arguments: Dict[str, str] = {},
|
|
185
187
|
streaming: bool = False,
|
|
188
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = None,
|
|
186
189
|
) -> "QuestionMemory":
|
|
187
190
|
return QuestionMemory(
|
|
188
191
|
self,
|
|
@@ -192,6 +195,7 @@ class NoMemorySessionMemory(BaseSessionMemory):
|
|
|
192
195
|
headers=headers,
|
|
193
196
|
arguments=arguments,
|
|
194
197
|
streaming=streaming,
|
|
198
|
+
chat_history=chat_history,
|
|
195
199
|
)
|
|
196
200
|
|
|
197
201
|
async def save(self, question: "QuestionMemory") -> None:
|
|
@@ -250,6 +254,7 @@ class EphemeralSessionMemory(BaseSessionMemory):
|
|
|
250
254
|
headers: Dict[str, str] = {},
|
|
251
255
|
arguments: Dict[str, str] = {},
|
|
252
256
|
streaming: bool = False,
|
|
257
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = None,
|
|
253
258
|
) -> "QuestionMemory":
|
|
254
259
|
return QuestionMemory(
|
|
255
260
|
self,
|
|
@@ -259,6 +264,7 @@ class EphemeralSessionMemory(BaseSessionMemory):
|
|
|
259
264
|
headers=headers,
|
|
260
265
|
arguments=arguments,
|
|
261
266
|
streaming=streaming,
|
|
267
|
+
chat_history=chat_history,
|
|
262
268
|
)
|
|
263
269
|
|
|
264
270
|
async def save(self, question: "QuestionMemory") -> None:
|
|
@@ -580,10 +586,21 @@ class QuestionMemory:
|
|
|
580
586
|
headers: Dict[str, str] | None = None,
|
|
581
587
|
arguments: Dict[str, str] | None = None,
|
|
582
588
|
streaming: bool = False,
|
|
589
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = None,
|
|
583
590
|
):
|
|
584
591
|
self.session = session
|
|
585
592
|
self.started_at = datetime.now(timezone.utc)
|
|
586
593
|
|
|
594
|
+
# Client-managed chat history. When set (even to an empty list), overrides
|
|
595
|
+
# server-side session history for agents that use previous Q&A context
|
|
596
|
+
# (rephrase, summarize, smart, etc.). None means "not set — use server-side
|
|
597
|
+
# history". [] means "override with no history". Intended for ephemeral
|
|
598
|
+
# sessions where the client is responsible for maintaining conversation state.
|
|
599
|
+
# Note: search_in_questions() performs semantic search over NucliaDB-stored
|
|
600
|
+
# conversation history and is NOT affected by this field. The HistoricalAgent
|
|
601
|
+
# uses that method and therefore does not benefit from client-managed history.
|
|
602
|
+
self._client_chat_history: Optional[List[HistoryQuestionAnswer]] = chat_history
|
|
603
|
+
|
|
587
604
|
# Start of a new question by the user
|
|
588
605
|
self.original_question = question
|
|
589
606
|
if actions is not None:
|
|
@@ -622,6 +639,10 @@ class QuestionMemory:
|
|
|
622
639
|
"""Returns the workflow ID for the current question. The workflow ID is a unique identifier that is shared across all questions and interactions that belong to the same workflow. This can be used to group related interactions together, and to keep track of the conversation history in a coherent way."""
|
|
623
640
|
return self.session.workflow_id
|
|
624
641
|
|
|
642
|
+
def get_account_id(self) -> str:
|
|
643
|
+
"""Returns the account ID for the current question."""
|
|
644
|
+
return self.session.account_id
|
|
645
|
+
|
|
625
646
|
def context_user_info(self) -> str:
|
|
626
647
|
"""Returns a string with user information that can be used in the context of the agent. This can include information such as user preferences, user history, or any other relevant information about the user that can help the agent to generate a more personalized and accurate response."""
|
|
627
648
|
return self.session.context_user_info()
|
|
@@ -649,11 +670,19 @@ class QuestionMemory:
|
|
|
649
670
|
return await self.session.get_source(source_id)
|
|
650
671
|
|
|
651
672
|
async def context_history(self) -> Tuple[str, int]:
|
|
652
|
-
"""Returns a string with the context history of the conversation. This can include information such as previous questions and answers, relevant information that has been previously discussed in the conversation, or any other relevant information that can help the agent to generate a more accurate and personalized response.
|
|
673
|
+
"""Returns a string with the context history of the conversation. This can include information such as previous questions and answers, relevant information that has been previously discussed in the conversation, or any other relevant information that can help the agent to generate a more accurate and personalized response.
|
|
674
|
+
|
|
675
|
+
When the client sets chat_history in the request (even to an empty list), it overrides any server-side session history. None means "not set — use server-side history"."""
|
|
676
|
+
if self._client_chat_history is not None:
|
|
677
|
+
return _qa_list_to_context_string(self._client_chat_history)
|
|
653
678
|
return await self.session.context_history()
|
|
654
679
|
|
|
655
680
|
async def get_chat_history(self) -> list[Message]:
|
|
656
|
-
"""Returns a list of tuples with the chat history of the conversation. Each tuple contains a question and an answer. This can be used to keep track of the conversation history in a more structured way, and to provide more context to the agent when generating a response.
|
|
681
|
+
"""Returns a list of tuples with the chat history of the conversation. Each tuple contains a question and an answer. This can be used to keep track of the conversation history in a more structured way, and to provide more context to the agent when generating a response.
|
|
682
|
+
|
|
683
|
+
When the client sets chat_history in the request (even to an empty list), it overrides any server-side session history. None means "not set — use server-side history"."""
|
|
684
|
+
if self._client_chat_history is not None:
|
|
685
|
+
return _qa_list_to_chat_messages(self._client_chat_history)
|
|
657
686
|
return await self.session.get_chat_history()
|
|
658
687
|
|
|
659
688
|
def stats(self):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Annotated, Dict, Literal
|
|
1
|
+
from typing import Annotated, Dict, List, Literal, Optional
|
|
2
2
|
|
|
3
3
|
from pydantic import AliasChoices, BaseModel, Field
|
|
4
4
|
from pydantic.types import Discriminator, Tag
|
|
@@ -8,6 +8,7 @@ from hyperforge.interaction import (
|
|
|
8
8
|
Feedback,
|
|
9
9
|
OAuthAuthenticateURL,
|
|
10
10
|
)
|
|
11
|
+
from hyperforge.models import HistoryQuestionAnswer
|
|
11
12
|
|
|
12
13
|
# Messages used in the pubsub protocol between API and agent servers
|
|
13
14
|
|
|
@@ -22,6 +23,10 @@ class StartInteraction(BaseModel):
|
|
|
22
23
|
question: str
|
|
23
24
|
headers: Dict[str, str] = {}
|
|
24
25
|
arguments: Dict[str, str] = {}
|
|
26
|
+
chat_history: Optional[List[HistoryQuestionAnswer]] = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
description="Client-managed chat history. When set (even to an empty list), overrides any server-side session history for agents that use previous Q&A context. Omit the field entirely to use server-side session history.",
|
|
29
|
+
)
|
|
25
30
|
workflow_id: str = "default"
|
|
26
31
|
streaming: bool = False
|
|
27
32
|
op: Literal["start"] = "start"
|
|
@@ -181,8 +181,12 @@ class SessionManager:
|
|
|
181
181
|
config=config.memory,
|
|
182
182
|
agent=message.agent_id,
|
|
183
183
|
workflow_id=message.workflow_id,
|
|
184
|
+
account_id=message.account,
|
|
184
185
|
)
|
|
185
|
-
|
|
186
|
+
# Ephemeral sessions are short-lived and should not be persisted in
|
|
187
|
+
# the shared LRU cache across activations.
|
|
188
|
+
if message.session != "ephemeral":
|
|
189
|
+
self.memory[message.session] = memory
|
|
186
190
|
else:
|
|
187
191
|
memory = self.memory[message.session]
|
|
188
192
|
|
|
@@ -194,6 +198,7 @@ class SessionManager:
|
|
|
194
198
|
headers=message.headers,
|
|
195
199
|
arguments=message.arguments,
|
|
196
200
|
streaming=message.streaming,
|
|
201
|
+
chat_history=message.chat_history,
|
|
197
202
|
)
|
|
198
203
|
|
|
199
204
|
task = asyncio.create_task(
|
|
@@ -16,6 +16,7 @@ async def get_memory(
|
|
|
16
16
|
config: MemoryConfig,
|
|
17
17
|
agent: str,
|
|
18
18
|
workflow_id: str,
|
|
19
|
+
account_id: str = "",
|
|
19
20
|
) -> BaseSessionMemory:
|
|
20
21
|
memory: BaseSessionMemory
|
|
21
22
|
|
|
@@ -53,5 +54,6 @@ async def get_memory(
|
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
memory.init(session=session)
|
|
57
|
+
memory.account_id = account_id
|
|
56
58
|
|
|
57
59
|
return memory
|
|
@@ -2,17 +2,23 @@ from pydantic import model_validator
|
|
|
2
2
|
from pydantic_settings import BaseSettings
|
|
3
3
|
|
|
4
4
|
_REDIRECT_PATH = "/api/auth/agent/{agent_id}/workflow/{workflow_id}/session/{session_id}/oauth/{oauth_uuid}/callback"
|
|
5
|
+
_MCP_CALLBACK_PATH = "/api/auth/mcp/callback"
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class OAuthSettings(BaseSettings):
|
|
8
9
|
nuclia_public_url: str = "https://{zone}.nuclia.com"
|
|
9
10
|
nuclia_zone: str = "arag"
|
|
10
11
|
rao_redirect_url: str = ""
|
|
12
|
+
mcp_callback_url: str = ""
|
|
11
13
|
|
|
12
14
|
@model_validator(mode="after")
|
|
13
15
|
def _resolve_urls(self) -> "OAuthSettings":
|
|
14
16
|
self.nuclia_public_url = self.nuclia_public_url.format(zone=self.nuclia_zone)
|
|
15
17
|
if not self.rao_redirect_url:
|
|
16
18
|
self.rao_redirect_url = self.nuclia_public_url.rstrip("/") + _REDIRECT_PATH
|
|
19
|
+
if not self.mcp_callback_url:
|
|
20
|
+
self.mcp_callback_url = (
|
|
21
|
+
self.nuclia_public_url.rstrip("/") + _MCP_CALLBACK_PATH
|
|
22
|
+
)
|
|
17
23
|
|
|
18
24
|
return self
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from fastapi import Query
|
|
4
|
-
from starlette.requests import Request
|
|
5
|
-
from starlette.responses import HTMLResponse
|
|
6
|
-
|
|
7
|
-
from hyperforge.api.settings import Settings
|
|
8
|
-
from hyperforge.api.v1.router import router
|
|
9
|
-
from hyperforge.api.v1.utils import tracer
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
RENDER = "<html><body><h1>OAuth Completed</h1><p>You can close this window and return to the application.</p></body></html>"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@router.get(
|
|
17
|
-
"/api/auth/agent/{agent_id}/workflow/{workflow_id}/session/{session}/oauth/{oauth_uuid}/callback",
|
|
18
|
-
status_code=200,
|
|
19
|
-
description="Get Agent Schema",
|
|
20
|
-
tags=["Retrieval Agent"],
|
|
21
|
-
include_in_schema=False,
|
|
22
|
-
)
|
|
23
|
-
async def oauth_callback(
|
|
24
|
-
request: Request,
|
|
25
|
-
agent_id: str,
|
|
26
|
-
session: str,
|
|
27
|
-
workflow_id: str,
|
|
28
|
-
oauth_uuid: str,
|
|
29
|
-
question_id: str = Query(..., include_in_schema=False),
|
|
30
|
-
state: str = Query(..., include_in_schema=False),
|
|
31
|
-
account_id: str = Query(..., include_in_schema=False),
|
|
32
|
-
):
|
|
33
|
-
"""
|
|
34
|
-
Callback from oauth flow on RAO that requires to send creds to websocket
|
|
35
|
-
"""
|
|
36
|
-
settings: Settings = request.app.settings
|
|
37
|
-
subject = settings.oauth_subject.format(
|
|
38
|
-
account=account_id,
|
|
39
|
-
agent_id=agent_id,
|
|
40
|
-
session=session,
|
|
41
|
-
question=question_id,
|
|
42
|
-
oauth_uuid=oauth_uuid,
|
|
43
|
-
workflow_id=workflow_id,
|
|
44
|
-
)
|
|
45
|
-
# Request a question
|
|
46
|
-
with tracer().start_as_current_span("Request activation"):
|
|
47
|
-
logger.info(
|
|
48
|
-
"OAuth callback received for agent=%s, session=%s, oauth_uuid=%s, question_id=%s",
|
|
49
|
-
agent_id,
|
|
50
|
-
session,
|
|
51
|
-
oauth_uuid,
|
|
52
|
-
question_id,
|
|
53
|
-
)
|
|
54
|
-
await request.app.broker.send_reply(subject, state)
|
|
55
|
-
logger.info(
|
|
56
|
-
"OAuth callback published to stream %s",
|
|
57
|
-
subject,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
return HTMLResponse(content=RENDER)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/b8edb72295e0_.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/mcp_interaction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-DLejS5jC.js
RENAMED
|
File without changes
|
{hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-MoxkZ1_3.css
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|