hyperforge 1.0.0.post50__tar.gz → 1.0.0.post56__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 (105) hide show
  1. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/PKG-INFO +3 -3
  2. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/pyproject.toml +3 -3
  3. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/models.py +5 -1
  4. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/interaction.py +1 -0
  5. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/broker/redis.py +1 -0
  6. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/engine.py +6 -2
  7. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/manager.py +3 -0
  8. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/memory/memory.py +49 -25
  9. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/pubsub.py +6 -1
  10. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/session.py +1 -0
  11. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/README.md +0 -0
  12. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/VERSION +0 -0
  13. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/__init__.py +0 -0
  14. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/agent.py +0 -0
  15. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/README +0 -0
  16. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/env.py +0 -0
  17. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/script.py.mako +0 -0
  18. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/049a4aed19e1_create_generation_table.py +0 -0
  19. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/1416cb41bd49_add_downloads_table.py +0 -0
  20. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/2847934e2d59_unique_driver.py +0 -0
  21. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/4d89a36c8bda_memory.py +0 -0
  22. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/72b997edb05d_identifier_for_drivers.py +0 -0
  23. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/9c6f6a1b4e7f_workflows_default_fk.py +0 -0
  24. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/b8edb72295e0_.py +0 -0
  25. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic/versions/f3d2c1b0a9e8_soft_delete_workflows.py +0 -0
  26. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/alembic.ini +0 -0
  27. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/__init__.py +0 -0
  28. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/app.py +0 -0
  29. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/authentication.py +0 -0
  30. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/commands.py +0 -0
  31. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/internal/__init__.py +0 -0
  32. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/internal/inspect.py +0 -0
  33. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/internal/router.py +0 -0
  34. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/logging.py +0 -0
  35. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/session.py +0 -0
  36. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/settings.py +0 -0
  37. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/utils.py +0 -0
  38. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/__init__.py +0 -0
  39. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/agents.py +0 -0
  40. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/mcp_content.py +0 -0
  41. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/mcp_interaction.py +0 -0
  42. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/oauth.py +0 -0
  43. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/prompt.py +0 -0
  44. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/router.py +0 -0
  45. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/schema.py +0 -0
  46. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/session.py +0 -0
  47. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/utils.py +0 -0
  48. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/api/v1/workflows.py +0 -0
  49. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/arag.py +0 -0
  50. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/broker/__init__.py +0 -0
  51. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/broker/local.py +0 -0
  52. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/configure.py +0 -0
  53. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/context/__init__.py +0 -0
  54. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/context/agent.py +0 -0
  55. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/context/config.py +0 -0
  56. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/database.py +0 -0
  57. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/__init__.py +0 -0
  58. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/agents.py +0 -0
  59. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/encryption.py +0 -0
  60. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/exceptions.py +0 -0
  61. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/settings.py +0 -0
  62. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/db/workflow_cleanup.py +0 -0
  63. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/definition.py +0 -0
  64. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/driver.py +0 -0
  65. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/dummy.py +0 -0
  66. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/exceptions.py +0 -0
  67. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/feature_flag.py +0 -0
  68. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/fixtures.py +0 -0
  69. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/interaction.py +0 -0
  70. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/llm.py +0 -0
  71. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/memory/__init__.py +0 -0
  72. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/minimal_fixtures.py +0 -0
  73. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/models.py +0 -0
  74. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/nua.py +0 -0
  75. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/openapi.py +0 -0
  76. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/prompts.py +0 -0
  77. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/py.typed +0 -0
  78. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/redis_utils.py +0 -0
  79. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/retrieval/__init__.py +0 -0
  80. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/retrieval/agent.py +0 -0
  81. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/retrieval/config.py +0 -0
  82. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/__init__.py +0 -0
  83. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/cache.py +0 -0
  84. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/run.py +0 -0
  85. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/sandbox.py +0 -0
  86. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/settings.py +0 -0
  87. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/utils.py +0 -0
  88. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/server/web.py +0 -0
  89. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/settings.py +0 -0
  90. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/__init__.py +0 -0
  91. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/agent.py +0 -0
  92. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/app.py +0 -0
  93. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/config.py +0 -0
  94. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/const.py +0 -0
  95. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/run.py +0 -0
  96. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/settings.py +0 -0
  97. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/standalone/ui_router.py +0 -0
  98. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/static/assets/index-DLejS5jC.js +0 -0
  99. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/static/assets/index-MoxkZ1_3.css +0 -0
  100. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/static/favicon.svg +0 -0
  101. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/static/index.html +0 -0
  102. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/trace.py +0 -0
  103. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/utils/__init__.py +0 -0
  104. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/src/hyperforge/utils/http.py +0 -0
  105. {hyperforge-1.0.0.post50 → hyperforge-1.0.0.post56}/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.post50
3
+ Version: 1.0.0.post56
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>
@@ -9,8 +9,8 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Dist: alembic
11
11
  Requires-Dist: pydantic
12
- Requires-Dist: nuclia>=4.9.25
13
- Requires-Dist: nuclia-models>=0.52.1
12
+ Requires-Dist: nuclia>=4.11.5
13
+ Requires-Dist: nuclia-models>=0.59.0
14
14
  Requires-Dist: nucliadb-models>=6.13.1.post6414
15
15
  Requires-Dist: nucliadb-sdk>=6.13.1.post6414
16
16
  Requires-Dist: nucliadb-telemetry[otel]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge"
7
- version = "1.0.0.post50"
7
+ version = "1.0.0.post56"
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"
@@ -17,8 +17,8 @@ license = "Apache-2.0"
17
17
  dependencies = [
18
18
  "alembic",
19
19
  "pydantic",
20
- "nuclia>=4.9.25",
21
- "nuclia-models>=0.52.1",
20
+ "nuclia>=4.11.5",
21
+ "nuclia-models>=0.59.0",
22
22
  "nucliadb-models>=6.13.1.post6414",
23
23
  "nucliadb-sdk>=6.13.1.post6414",
24
24
  "nucliadb-telemetry[otel]",
@@ -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
  )
@@ -43,6 +43,7 @@ class RedisBroker(Broker):
43
43
  client = Redis.from_url(
44
44
  url,
45
45
  decode_responses=True,
46
+ socket_timeout=keepalive_ms / 1000,
46
47
  )
47
48
  return cls(client, activate_subject, keepalive_ms)
48
49
 
@@ -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
 
@@ -196,6 +196,9 @@ class Manager:
196
196
  result.object = c.object
197
197
  elif isinstance(c, MetaGenerativeResponse):
198
198
  result.timings = c.timings
199
+ result.learning_id = c.learning_id
200
+ result.model_name = c.model_name
201
+ result.trace_id = c.trace_id
199
202
  elif isinstance(c, CitationsGenerativeResponse):
200
203
  result.citations = c.citations
201
204
  elif isinstance(c, FootnoteCitationsGenerativeResponse):
@@ -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"
@@ -112,34 +132,13 @@ class BaseSessionMemory:
112
132
  return KnowledgeboxFindResults(total=0, resources={})
113
133
 
114
134
  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
135
+ return _qa_list_to_chat_messages(await self.qa_history())
131
136
 
132
137
  async def qa_history(self) -> list[HistoryQuestionAnswer]:
133
138
  return []
134
139
 
135
140
  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
141
+ return _qa_list_to_context_string(await self.qa_history())
143
142
 
144
143
  def start_question(
145
144
  self,
@@ -149,6 +148,7 @@ class BaseSessionMemory:
149
148
  headers: Dict[str, str] = {},
150
149
  arguments: Dict[str, str] = {},
151
150
  streaming: bool = False,
151
+ chat_history: Optional[List[HistoryQuestionAnswer]] = None,
152
152
  ) -> "QuestionMemory":
153
153
  return QuestionMemory(
154
154
  self,
@@ -158,6 +158,7 @@ class BaseSessionMemory:
158
158
  headers=headers,
159
159
  arguments=arguments,
160
160
  streaming=streaming,
161
+ chat_history=chat_history,
161
162
  )
162
163
 
163
164
  async def save(self, question: "QuestionMemory") -> None:
@@ -183,6 +184,7 @@ class NoMemorySessionMemory(BaseSessionMemory):
183
184
  headers: Dict[str, str] = {},
184
185
  arguments: Dict[str, str] = {},
185
186
  streaming: bool = False,
187
+ chat_history: Optional[List[HistoryQuestionAnswer]] = None,
186
188
  ) -> "QuestionMemory":
187
189
  return QuestionMemory(
188
190
  self,
@@ -192,6 +194,7 @@ class NoMemorySessionMemory(BaseSessionMemory):
192
194
  headers=headers,
193
195
  arguments=arguments,
194
196
  streaming=streaming,
197
+ chat_history=chat_history,
195
198
  )
196
199
 
197
200
  async def save(self, question: "QuestionMemory") -> None:
@@ -250,6 +253,7 @@ class EphemeralSessionMemory(BaseSessionMemory):
250
253
  headers: Dict[str, str] = {},
251
254
  arguments: Dict[str, str] = {},
252
255
  streaming: bool = False,
256
+ chat_history: Optional[List[HistoryQuestionAnswer]] = None,
253
257
  ) -> "QuestionMemory":
254
258
  return QuestionMemory(
255
259
  self,
@@ -259,6 +263,7 @@ class EphemeralSessionMemory(BaseSessionMemory):
259
263
  headers=headers,
260
264
  arguments=arguments,
261
265
  streaming=streaming,
266
+ chat_history=chat_history,
262
267
  )
263
268
 
264
269
  async def save(self, question: "QuestionMemory") -> None:
@@ -580,10 +585,21 @@ class QuestionMemory:
580
585
  headers: Dict[str, str] | None = None,
581
586
  arguments: Dict[str, str] | None = None,
582
587
  streaming: bool = False,
588
+ chat_history: Optional[List[HistoryQuestionAnswer]] = None,
583
589
  ):
584
590
  self.session = session
585
591
  self.started_at = datetime.now(timezone.utc)
586
592
 
593
+ # Client-managed chat history. When set (even to an empty list), overrides
594
+ # server-side session history for agents that use previous Q&A context
595
+ # (rephrase, summarize, smart, etc.). None means "not set — use server-side
596
+ # history". [] means "override with no history". Intended for ephemeral
597
+ # sessions where the client is responsible for maintaining conversation state.
598
+ # Note: search_in_questions() performs semantic search over NucliaDB-stored
599
+ # conversation history and is NOT affected by this field. The HistoricalAgent
600
+ # uses that method and therefore does not benefit from client-managed history.
601
+ self._client_chat_history: Optional[List[HistoryQuestionAnswer]] = chat_history
602
+
587
603
  # Start of a new question by the user
588
604
  self.original_question = question
589
605
  if actions is not None:
@@ -649,11 +665,19 @@ class QuestionMemory:
649
665
  return await self.session.get_source(source_id)
650
666
 
651
667
  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."""
668
+ """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.
669
+
670
+ 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"."""
671
+ if self._client_chat_history is not None:
672
+ return _qa_list_to_context_string(self._client_chat_history)
653
673
  return await self.session.context_history()
654
674
 
655
675
  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."""
676
+ """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.
677
+
678
+ 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"."""
679
+ if self._client_chat_history is not None:
680
+ return _qa_list_to_chat_messages(self._client_chat_history)
657
681
  return await self.session.get_chat_history()
658
682
 
659
683
  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"
@@ -194,6 +194,7 @@ class SessionManager:
194
194
  headers=message.headers,
195
195
  arguments=message.arguments,
196
196
  streaming=message.streaming,
197
+ chat_history=message.chat_history,
197
198
  )
198
199
 
199
200
  task = asyncio.create_task(