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.
Files changed (106) hide show
  1. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/PKG-INFO +1 -1
  2. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/pyproject.toml +1 -1
  3. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/models.py +5 -1
  4. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/interaction.py +1 -0
  5. hyperforge-1.0.0.post57/src/hyperforge/api/v1/oauth.py +163 -0
  6. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/engine.py +6 -2
  7. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/interaction.py +1 -0
  8. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/memory/memory.py +54 -25
  9. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/pubsub.py +6 -1
  10. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/session.py +6 -1
  11. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/utils.py +2 -0
  12. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/settings.py +6 -0
  13. hyperforge-1.0.0.post55/src/hyperforge/api/v1/oauth.py +0 -60
  14. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/README.md +0 -0
  15. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/VERSION +0 -0
  16. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/__init__.py +0 -0
  17. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/agent.py +0 -0
  18. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/README +0 -0
  19. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/env.py +0 -0
  20. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/script.py.mako +0 -0
  21. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/049a4aed19e1_create_generation_table.py +0 -0
  22. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/1416cb41bd49_add_downloads_table.py +0 -0
  23. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/2847934e2d59_unique_driver.py +0 -0
  24. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/4d89a36c8bda_memory.py +0 -0
  25. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/72b997edb05d_identifier_for_drivers.py +0 -0
  26. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/9c6f6a1b4e7f_workflows_default_fk.py +0 -0
  27. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/b8edb72295e0_.py +0 -0
  28. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic/versions/f3d2c1b0a9e8_soft_delete_workflows.py +0 -0
  29. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/alembic.ini +0 -0
  30. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/__init__.py +0 -0
  31. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/app.py +0 -0
  32. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/authentication.py +0 -0
  33. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/commands.py +0 -0
  34. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/__init__.py +0 -0
  35. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/inspect.py +0 -0
  36. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/internal/router.py +0 -0
  37. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/logging.py +0 -0
  38. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/session.py +0 -0
  39. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/settings.py +0 -0
  40. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/utils.py +0 -0
  41. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/__init__.py +0 -0
  42. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/agents.py +0 -0
  43. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/mcp_content.py +0 -0
  44. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/mcp_interaction.py +0 -0
  45. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/prompt.py +0 -0
  46. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/router.py +0 -0
  47. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/schema.py +0 -0
  48. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/session.py +0 -0
  49. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/utils.py +0 -0
  50. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/api/v1/workflows.py +0 -0
  51. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/arag.py +0 -0
  52. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/__init__.py +0 -0
  53. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/local.py +0 -0
  54. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/broker/redis.py +0 -0
  55. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/configure.py +0 -0
  56. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/__init__.py +0 -0
  57. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/agent.py +0 -0
  58. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/context/config.py +0 -0
  59. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/database.py +0 -0
  60. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/__init__.py +0 -0
  61. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/agents.py +0 -0
  62. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/encryption.py +0 -0
  63. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/exceptions.py +0 -0
  64. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/settings.py +0 -0
  65. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/db/workflow_cleanup.py +0 -0
  66. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/definition.py +0 -0
  67. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/driver.py +0 -0
  68. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/dummy.py +0 -0
  69. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/exceptions.py +0 -0
  70. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/feature_flag.py +0 -0
  71. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/fixtures.py +0 -0
  72. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/llm.py +0 -0
  73. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/manager.py +0 -0
  74. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/memory/__init__.py +0 -0
  75. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/minimal_fixtures.py +0 -0
  76. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/models.py +0 -0
  77. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/nua.py +0 -0
  78. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/openapi.py +0 -0
  79. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/prompts.py +0 -0
  80. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/py.typed +0 -0
  81. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/redis_utils.py +0 -0
  82. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/__init__.py +0 -0
  83. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/agent.py +0 -0
  84. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/retrieval/config.py +0 -0
  85. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/__init__.py +0 -0
  86. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/cache.py +0 -0
  87. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/run.py +0 -0
  88. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/sandbox.py +0 -0
  89. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/settings.py +0 -0
  90. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/server/web.py +0 -0
  91. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/__init__.py +0 -0
  92. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/agent.py +0 -0
  93. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/app.py +0 -0
  94. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/config.py +0 -0
  95. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/const.py +0 -0
  96. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/run.py +0 -0
  97. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/settings.py +0 -0
  98. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/standalone/ui_router.py +0 -0
  99. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-DLejS5jC.js +0 -0
  100. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/assets/index-MoxkZ1_3.css +0 -0
  101. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/favicon.svg +0 -0
  102. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/static/index.html +0 -0
  103. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/trace.py +0 -0
  104. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/utils/__init__.py +0 -0
  105. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/utils/http.py +0 -0
  106. {hyperforge-1.0.0.post55 → hyperforge-1.0.0.post57}/src/hyperforge/workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge
3
- Version: 1.0.0.post55
3
+ Version: 1.0.0.post57
4
4
  Summary: Agentic Framework for Orchestrated Runtime, Governance, and Execution
5
5
  Author: AI Data Team
6
6
  Author-email: AI Data Team <learning@nuclia.com>
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge"
7
- version = "1.0.0.post55"
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
@@ -208,6 +208,7 @@ async def stream_response(
208
208
  question=interaction.question,
209
209
  headers=interaction.headers,
210
210
  arguments=interaction.arguments,
211
+ chat_history=interaction.chat_history,
211
212
  workflow_id=workflow_id,
212
213
  streaming=interaction.streaming,
213
214
  )
@@ -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(question, streaming=streaming)
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
 
@@ -60,6 +60,7 @@ class Provider(Enum):
60
60
  AZURE_CERTIFICATE_CREDENTIALS = "azure_certificate_credentials"
61
61
  AWS_S3_ACCESS_KEYS = "aws_s3_access_keys"
62
62
  SHAREFILE_OAUTH = "sharefile_oauth"
63
+ MCP_OAUTH = "mcp_oauth"
63
64
 
64
65
 
65
66
  class OAuthAuthenticateURL(BaseModel):
@@ -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
- qas = await self.qa_history()
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
- result = ""
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
- self.memory[message.session] = memory
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)