fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
194 lines
7.8 KiB
Python
194 lines
7.8 KiB
Python
import logging
|
|
import traceback
|
|
|
|
from flask import request, Response
|
|
from flask_restx import fields, Resource
|
|
|
|
from application.api import api
|
|
|
|
from application.api.answer.routes.base import answer_ns, BaseAnswerResource
|
|
|
|
from application.api.answer.services.persistence_policy import resolve_persistence
|
|
from application.api.answer.services.stream_processor import StreamProcessor
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@answer_ns.route("/stream")
|
|
class StreamResource(Resource, BaseAnswerResource):
|
|
def __init__(self, *args, **kwargs):
|
|
Resource.__init__(self, *args, **kwargs)
|
|
BaseAnswerResource.__init__(self)
|
|
|
|
stream_model = answer_ns.model(
|
|
"StreamModel",
|
|
{
|
|
"question": fields.String(
|
|
required=True, description="Question to be asked"
|
|
),
|
|
"history": fields.List(
|
|
fields.String,
|
|
required=False,
|
|
description="Conversation history (only for new conversations)",
|
|
),
|
|
"conversation_id": fields.String(
|
|
required=False,
|
|
description="Existing conversation ID (loads history)",
|
|
),
|
|
"prompt_id": fields.String(
|
|
required=False, default="default", description="Prompt ID"
|
|
),
|
|
"chunks": fields.Integer(
|
|
required=False, default=2, description="Number of chunks"
|
|
),
|
|
"retriever": fields.String(required=False, description="Retriever type"),
|
|
"api_key": fields.String(required=False, description="API key"),
|
|
"agent_id": fields.String(required=False, description="Agent ID"),
|
|
"active_docs": fields.String(
|
|
required=False, description="Active documents"
|
|
),
|
|
"isNoneDoc": fields.Boolean(
|
|
required=False, description="Flag indicating if no document is used"
|
|
),
|
|
"index": fields.Integer(
|
|
required=False, description="Index of the query to update"
|
|
),
|
|
"save_conversation": fields.Boolean(
|
|
required=False,
|
|
description=(
|
|
"Deprecated, no effect: conversations always persist. "
|
|
"Use `visibility` to control sidebar listing."
|
|
),
|
|
),
|
|
"visibility": fields.String(
|
|
required=False,
|
|
default="hidden",
|
|
description=(
|
|
"'listed' shows the conversation in the owner's sidebar; "
|
|
"any other value (or omitting it) persists it hidden."
|
|
),
|
|
),
|
|
"model_id": fields.String(
|
|
required=False,
|
|
description="Model ID to use for this request",
|
|
),
|
|
"attachments": fields.List(
|
|
fields.String, required=False, description="List of attachment IDs"
|
|
),
|
|
"passthrough": fields.Raw(
|
|
required=False,
|
|
description="Dynamic parameters to inject into prompt template",
|
|
),
|
|
},
|
|
)
|
|
|
|
@api.expect(stream_model)
|
|
@api.doc(description="Stream a response based on the question and retriever")
|
|
def post(self):
|
|
data = request.get_json()
|
|
if error := self.validate_request(data, "index" in data):
|
|
return error
|
|
decoded_token = getattr(request, "decoded_token", None)
|
|
processor = StreamProcessor(data, decoded_token)
|
|
|
|
try:
|
|
# ---- Continuation mode ----
|
|
if data.get("tool_actions"):
|
|
(
|
|
agent,
|
|
messages,
|
|
tools_dict,
|
|
pending_tool_calls,
|
|
tool_actions,
|
|
reasoning_content,
|
|
) = processor.resume_from_tool_actions(
|
|
data["tool_actions"], data["conversation_id"]
|
|
)
|
|
if not processor.decoded_token:
|
|
return Response(
|
|
self.error_stream_generate("Unauthorized"),
|
|
status=401,
|
|
mimetype="text/event-stream",
|
|
)
|
|
if error := self.check_usage(processor.agent_config):
|
|
return error
|
|
return Response(
|
|
self.complete_stream(
|
|
question="",
|
|
agent=agent,
|
|
conversation_id=processor.conversation_id,
|
|
user_api_key=processor.agent_config.get("user_api_key"),
|
|
decoded_token=processor.decoded_token,
|
|
agent_id=processor.agent_id,
|
|
model_id=processor.model_id,
|
|
model_user_id=processor.model_user_id,
|
|
_continuation={
|
|
"messages": messages,
|
|
"tools_dict": tools_dict,
|
|
"pending_tool_calls": pending_tool_calls,
|
|
"tool_actions": tool_actions,
|
|
"reserved_message_id": processor.reserved_message_id,
|
|
"request_id": processor.request_id,
|
|
"reasoning_content": reasoning_content,
|
|
},
|
|
),
|
|
mimetype="text/event-stream",
|
|
)
|
|
|
|
# ---- Normal mode ----
|
|
agent = processor.build_agent(data["question"])
|
|
if not processor.decoded_token:
|
|
return Response(
|
|
self.error_stream_generate("Unauthorized"),
|
|
status=401,
|
|
mimetype="text/event-stream",
|
|
)
|
|
|
|
if error := self.check_usage(processor.agent_config):
|
|
return error
|
|
should_persist, visibility = resolve_persistence(
|
|
visibility_flag=data.get("visibility"),
|
|
persist_flag=data.get("persist"),
|
|
)
|
|
return Response(
|
|
self.complete_stream(
|
|
question=data["question"],
|
|
agent=agent,
|
|
conversation_id=processor.conversation_id,
|
|
user_api_key=processor.agent_config.get("user_api_key"),
|
|
decoded_token=processor.decoded_token,
|
|
isNoneDoc=data.get("isNoneDoc"),
|
|
index=data.get("index"),
|
|
should_persist=should_persist,
|
|
visibility=visibility,
|
|
attachment_ids=data.get("attachments", []),
|
|
agent_id=processor.agent_id,
|
|
is_shared_usage=processor.is_shared_usage,
|
|
shared_token=processor.shared_token,
|
|
model_id=processor.model_id,
|
|
model_user_id=processor.model_user_id,
|
|
),
|
|
mimetype="text/event-stream",
|
|
)
|
|
except ValueError as e:
|
|
message = "Malformed request body"
|
|
logger.error(
|
|
f"/stream - error: {message} - specific error: {str(e)} - traceback: {traceback.format_exc()}",
|
|
extra={"error": str(e), "traceback": traceback.format_exc()},
|
|
)
|
|
return Response(
|
|
self.error_stream_generate(message),
|
|
status=400,
|
|
mimetype="text/event-stream",
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
|
|
extra={"error": str(e), "traceback": traceback.format_exc()},
|
|
)
|
|
return Response(
|
|
self.error_stream_generate("Unknown error occurred"),
|
|
status=400,
|
|
mimetype="text/event-stream",
|
|
)
|