Skip to content

Commit 23aeaff

Browse files
authored
Merge pull request #2362 from arc53/v1-mini-improvements
feat: history overwrite
2 parents cdd6ff6 + 689dd79 commit 23aeaff

21 files changed

Lines changed: 638 additions & 71 deletions

File tree

application/api/answer/services/stream_processor.py

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def __init__(
112112
self._required_tool_actions: Optional[Dict[str, Set[Optional[str]]]] = None
113113
self.compressed_summary: Optional[str] = None
114114
self.compressed_summary_tokens: int = 0
115+
self._agent_data: Optional[Dict[str, Any]] = None
115116

116117
def initialize(self):
117118
"""Initialize all required components for processing"""
@@ -359,22 +360,29 @@ def _get_data_from_api_key(self, api_key: str) -> Dict[str, Any]:
359360
return data
360361

361362
def _configure_source(self):
362-
"""Configure the source based on agent data"""
363-
api_key = self.data.get("api_key") or self.agent_key
363+
"""Configure the source based on agent data.
364364
365-
if api_key:
366-
agent_data = self._get_data_from_api_key(api_key)
365+
The literal string ``"default"`` is a placeholder meaning "no
366+
ingested source" and is normalized to an empty source so that no
367+
retrieval is attempted.
368+
"""
369+
if self._agent_data:
370+
agent_data = self._agent_data
367371

368372
if agent_data.get("sources") and len(agent_data["sources"]) > 0:
369373
source_ids = [
370-
source["id"] for source in agent_data["sources"] if source.get("id")
374+
source["id"]
375+
for source in agent_data["sources"]
376+
if source.get("id") and source["id"] != "default"
371377
]
372378
if source_ids:
373379
self.source = {"active_docs": source_ids}
374380
else:
375381
self.source = {}
376-
self.all_sources = agent_data["sources"]
377-
elif agent_data.get("source"):
382+
self.all_sources = [
383+
s for s in agent_data["sources"] if s.get("id") != "default"
384+
]
385+
elif agent_data.get("source") and agent_data["source"] != "default":
378386
self.source = {"active_docs": agent_data["source"]}
379387
self.all_sources = [
380388
{
@@ -387,11 +395,24 @@ def _configure_source(self):
387395
self.all_sources = []
388396
return
389397
if "active_docs" in self.data:
390-
self.source = {"active_docs": self.data["active_docs"]}
398+
active_docs = self.data["active_docs"]
399+
if active_docs and active_docs != "default":
400+
self.source = {"active_docs": active_docs}
401+
else:
402+
self.source = {}
391403
return
392404
self.source = {}
393405
self.all_sources = []
394406

407+
def _has_active_docs(self) -> bool:
408+
"""Return True if a real document source is configured for retrieval."""
409+
active_docs = self.source.get("active_docs") if self.source else None
410+
if not active_docs:
411+
return False
412+
if active_docs == "default":
413+
return False
414+
return True
415+
395416
def _resolve_agent_id(self) -> Optional[str]:
396417
"""Resolve agent_id from request, then fall back to conversation context."""
397418
request_agent_id = self.data.get("agent_id")
@@ -433,48 +454,39 @@ def _configure_agent(self):
433454
effective_key = self.data.get("api_key") or self.agent_key
434455

435456
if effective_key:
436-
data_key = self._get_data_from_api_key(effective_key)
437-
if data_key.get("_id"):
438-
self.agent_id = str(data_key.get("_id"))
457+
self._agent_data = self._get_data_from_api_key(effective_key)
458+
if self._agent_data.get("_id"):
459+
self.agent_id = str(self._agent_data.get("_id"))
439460

440461
self.agent_config.update(
441462
{
442-
"prompt_id": data_key.get("prompt_id", "default"),
443-
"agent_type": data_key.get("agent_type", settings.AGENT_NAME),
463+
"prompt_id": self._agent_data.get("prompt_id", "default"),
464+
"agent_type": self._agent_data.get("agent_type", settings.AGENT_NAME),
444465
"user_api_key": effective_key,
445-
"json_schema": data_key.get("json_schema"),
446-
"default_model_id": data_key.get("default_model_id", ""),
447-
"models": data_key.get("models", []),
466+
"json_schema": self._agent_data.get("json_schema"),
467+
"default_model_id": self._agent_data.get("default_model_id", ""),
468+
"models": self._agent_data.get("models", []),
469+
"allow_system_prompt_override": self._agent_data.get(
470+
"allow_system_prompt_override", False
471+
),
448472
}
449473
)
450474

451475
# Set identity context
452476
if self.data.get("api_key"):
453477
# External API key: use the key owner's identity
454-
self.initial_user_id = data_key.get("user")
455-
self.decoded_token = {"sub": data_key.get("user")}
478+
self.initial_user_id = self._agent_data.get("user")
479+
self.decoded_token = {"sub": self._agent_data.get("user")}
456480
elif self.is_shared_usage:
457481
# Shared agent: keep the caller's identity
458482
pass
459483
else:
460484
# Owner using their own agent
461-
self.decoded_token = {"sub": data_key.get("user")}
462-
463-
if data_key.get("source"):
464-
self.source = {"active_docs": data_key["source"]}
465-
if data_key.get("workflow"):
466-
self.agent_config["workflow"] = data_key["workflow"]
467-
self.agent_config["workflow_owner"] = data_key.get("user")
468-
if data_key.get("retriever"):
469-
self.retriever_config["retriever_name"] = data_key["retriever"]
470-
if data_key.get("chunks") is not None:
471-
try:
472-
self.retriever_config["chunks"] = int(data_key["chunks"])
473-
except (ValueError, TypeError):
474-
logger.warning(
475-
f"Invalid chunks value: {data_key['chunks']}, using default value 2"
476-
)
477-
self.retriever_config["chunks"] = 2
485+
self.decoded_token = {"sub": self._agent_data.get("user")}
486+
487+
if self._agent_data.get("workflow"):
488+
self.agent_config["workflow"] = self._agent_data["workflow"]
489+
self.agent_config["workflow_owner"] = self._agent_data.get("user")
478490
else:
479491
# No API key — default/workflow configuration
480492
agent_type = settings.AGENT_NAME
@@ -497,14 +509,45 @@ def _configure_agent(self):
497509
)
498510

499511
def _configure_retriever(self):
512+
"""Assemble retriever config with precedence: request > agent > default."""
500513
doc_token_limit = calculate_doc_token_budget(model_id=self.model_id)
501514

515+
# Start with defaults
516+
retriever_name = "classic"
517+
chunks = 2
518+
519+
# Layer agent-level config (if present)
520+
if self._agent_data:
521+
if self._agent_data.get("retriever"):
522+
retriever_name = self._agent_data["retriever"]
523+
if self._agent_data.get("chunks") is not None:
524+
try:
525+
chunks = int(self._agent_data["chunks"])
526+
except (ValueError, TypeError):
527+
logger.warning(
528+
f"Invalid agent chunks value: {self._agent_data['chunks']}, "
529+
"using default value 2"
530+
)
531+
532+
# Explicit request values win over agent config
533+
if "retriever" in self.data:
534+
retriever_name = self.data["retriever"]
535+
if "chunks" in self.data:
536+
try:
537+
chunks = int(self.data["chunks"])
538+
except (ValueError, TypeError):
539+
logger.warning(
540+
f"Invalid request chunks value: {self.data['chunks']}, "
541+
"using default value 2"
542+
)
543+
502544
self.retriever_config = {
503-
"retriever_name": self.data.get("retriever", "classic"),
504-
"chunks": int(self.data.get("chunks", 2)),
545+
"retriever_name": retriever_name,
546+
"chunks": chunks,
505547
"doc_token_limit": doc_token_limit,
506548
}
507549

550+
# isNoneDoc without an API key forces no retrieval
508551
api_key = self.data.get("api_key") or self.agent_key
509552
if not api_key and "isNoneDoc" in self.data and self.data["isNoneDoc"]:
510553
self.retriever_config["chunks"] = 0
@@ -528,6 +571,9 @@ def pre_fetch_docs(self, question: str) -> tuple[Optional[str], Optional[list]]:
528571
if self.data.get("isNoneDoc", False) and not self.agent_id:
529572
logger.info("Pre-fetch skipped: isNoneDoc=True")
530573
return None, None
574+
if not self._has_active_docs():
575+
logger.info("Pre-fetch skipped: no active docs configured")
576+
return None, None
531577
try:
532578
retriever = self.create_retriever()
533579
logger.info(
@@ -910,15 +956,23 @@ def create_agent(
910956
raw_prompt = get_prompt(prompt_id, self.prompts_collection)
911957
self._prompt_content = raw_prompt
912958

913-
rendered_prompt = self.prompt_renderer.render_prompt(
914-
prompt_content=raw_prompt,
915-
user_id=self.initial_user_id,
916-
request_id=self.data.get("request_id"),
917-
passthrough_data=self.data.get("passthrough"),
918-
docs=docs,
919-
docs_together=docs_together,
920-
tools_data=tools_data,
921-
)
959+
# Allow API callers to override the system prompt when the agent
960+
# has opted in via allow_system_prompt_override.
961+
if (
962+
self.agent_config.get("allow_system_prompt_override", False)
963+
and self.data.get("system_prompt_override")
964+
):
965+
rendered_prompt = self.data["system_prompt_override"]
966+
else:
967+
rendered_prompt = self.prompt_renderer.render_prompt(
968+
prompt_content=raw_prompt,
969+
user_id=self.initial_user_id,
970+
request_id=self.data.get("request_id"),
971+
passthrough_data=self.data.get("passthrough"),
972+
docs=docs,
973+
docs_together=docs_together,
974+
tools_data=tools_data,
975+
)
922976

923977
provider = (
924978
get_provider_from_model_id(self.model_id)

application/api/user/agents/routes.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"token_limit",
7474
"limited_request_mode",
7575
"request_limit",
76+
"allow_system_prompt_override",
7677
"createdAt",
7778
"updatedAt",
7879
"lastUsedAt",
@@ -96,6 +97,7 @@
9697
"token_limit",
9798
"limited_request_mode",
9899
"request_limit",
100+
"allow_system_prompt_override",
99101
"createdAt",
100102
"updatedAt",
101103
"lastUsedAt",
@@ -220,6 +222,12 @@ def build_agent_document(
220222
base_doc["request_limit"] = int(
221223
data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"])
222224
)
225+
if "allow_system_prompt_override" in allowed_fields:
226+
base_doc["allow_system_prompt_override"] = (
227+
data.get("allow_system_prompt_override") == "True"
228+
if isinstance(data.get("allow_system_prompt_override"), str)
229+
else bool(data.get("allow_system_prompt_override", False))
230+
)
223231
return {k: v for k, v in base_doc.items() if k in allowed_fields}
224232

225233

@@ -292,6 +300,9 @@ def get(self):
292300
"default_model_id": agent.get("default_model_id", ""),
293301
"folder_id": agent.get("folder_id"),
294302
"workflow": agent.get("workflow"),
303+
"allow_system_prompt_override": agent.get(
304+
"allow_system_prompt_override", False
305+
),
295306
}
296307
return make_response(jsonify(data), 200)
297308
except Exception as e:
@@ -373,6 +384,9 @@ def get(self):
373384
"default_model_id": agent.get("default_model_id", ""),
374385
"folder_id": agent.get("folder_id"),
375386
"workflow": agent.get("workflow"),
387+
"allow_system_prompt_override": agent.get(
388+
"allow_system_prompt_override", False
389+
),
376390
}
377391
for agent in agents
378392
if "source" in agent
@@ -450,6 +464,10 @@ class CreateAgent(Resource):
450464
"folder_id": fields.String(
451465
required=False, description="Folder ID to organize the agent"
452466
),
467+
"allow_system_prompt_override": fields.Boolean(
468+
required=False,
469+
description="Allow API callers to override the system prompt via the v1 endpoint",
470+
),
453471
},
454472
)
455473

@@ -674,6 +692,10 @@ class UpdateAgent(Resource):
674692
"folder_id": fields.String(
675693
required=False, description="Folder ID to organize the agent"
676694
),
695+
"allow_system_prompt_override": fields.Boolean(
696+
required=False,
697+
description="Allow API callers to override the system prompt via the v1 endpoint",
698+
),
677699
},
678700
)
679701

@@ -765,6 +787,7 @@ def put(self, agent_id):
765787
"default_model_id",
766788
"folder_id",
767789
"workflow",
790+
"allow_system_prompt_override",
768791
]
769792

770793
for field in allowed_fields:
@@ -983,6 +1006,13 @@ def put(self, agent_id):
9831006
if workflow_error:
9841007
return workflow_error
9851008
update_fields[field] = workflow_id
1009+
elif field == "allow_system_prompt_override":
1010+
raw_value = data.get("allow_system_prompt_override", False)
1011+
update_fields[field] = (
1012+
raw_value == "True"
1013+
if isinstance(raw_value, str)
1014+
else bool(raw_value)
1015+
)
9861016
else:
9871017
value = data[field]
9881018
if field in ["name", "description", "prompt_id", "agent_type"]:

application/api/v1/routes.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,18 @@ def chat_completions():
138138
if usage_error:
139139
return usage_error
140140

141+
should_save_conversation = bool(internal_data.get("save_conversation", False))
142+
141143
if is_stream:
142144
return Response(
143145
_stream_response(
144-
helper, question, agent, processor, model_name, continuation
146+
helper,
147+
question,
148+
agent,
149+
processor,
150+
model_name,
151+
continuation,
152+
should_save_conversation,
145153
),
146154
mimetype="text/event-stream",
147155
headers={
@@ -151,7 +159,13 @@ def chat_completions():
151159
)
152160
else:
153161
return _non_stream_response(
154-
helper, question, agent, processor, model_name, continuation
162+
helper,
163+
question,
164+
agent,
165+
processor,
166+
model_name,
167+
continuation,
168+
should_save_conversation,
155169
)
156170

157171
except ValueError as e:
@@ -181,6 +195,7 @@ def _stream_response(
181195
processor: StreamProcessor,
182196
model_name: str,
183197
continuation: Optional[Dict],
198+
should_save_conversation: bool,
184199
) -> Generator[str, None, None]:
185200
"""Generate translated SSE chunks for streaming response."""
186201
completion_id = f"chatcmpl-{int(time.time())}"
@@ -193,6 +208,7 @@ def _stream_response(
193208
decoded_token=processor.decoded_token,
194209
agent_id=processor.agent_id,
195210
model_id=processor.model_id,
211+
should_save_conversation=should_save_conversation,
196212
_continuation=continuation,
197213
)
198214

@@ -225,6 +241,7 @@ def _non_stream_response(
225241
processor: StreamProcessor,
226242
model_name: str,
227243
continuation: Optional[Dict],
244+
should_save_conversation: bool,
228245
) -> Response:
229246
"""Collect full response and return as single JSON."""
230247
stream = helper.complete_stream(
@@ -235,6 +252,7 @@ def _non_stream_response(
235252
decoded_token=processor.decoded_token,
236253
agent_id=processor.agent_id,
237254
model_id=processor.model_id,
255+
should_save_conversation=should_save_conversation,
238256
_continuation=continuation,
239257
)
240258

0 commit comments

Comments
 (0)