File size: 11,867 Bytes
54eb2ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import json
from fastapi import HTTPException, Request
from fastapi.responses import StreamingResponse
from typing import List
from langchain_core.documents import Document
from ..schema.chat import ChatQueryRequest
from ..schema.message import MessageCreate
from ..schema.source import SourceCreate
from ..core.chat_engine.query import ChatEngine
from ..core.logger import SingletonLogger
from ..controller import message as message_controller
from ..controller import source as source_controller

logger = SingletonLogger().get_logger()


async def query_paper(user_id: int, payload: ChatQueryRequest, request: Request):
    """

    Query a paper using the chat engine with conversation history.

    Automatically saves user query and assistant response to message table.



    Args:

        user_id: ID of the user making the query

        payload: ChatQueryRequest containing query and parameters

        request: FastAPI Request object to access app state



    Returns:

        StreamingResponse with SSE updates from the LangGraph execution



    Raises:

        HTTPException: If there's an error processing the query

    """
    try:
        logger.info(
            f"User {user_id} querying paper {payload.paper_id} "
            f"in session {payload.session_id}: {payload.query}"
        )

        # API keys are decrypted by APIKeyDecryptionMiddleware and stored in request.state
        graph = request.app.state.graph

        async def stream_and_save():
            final_response = ""
            response_metadata = {}
            retrieved_docs: List[Document] = []
            web_search_results: List[Document] = []

            try:
                response_stream = ChatEngine.generate_response(
                    graph=graph,
                    query=payload.query,
                    user_id=user_id,
                    thread_id=payload.session_id,
                    paper_id=payload.paper_id,
                    model_name=payload.model_name,
                    temperature=payload.temperature,
                    max_tokens=payload.max_tokens,
                    top_k=payload.top_k,
                    use_web_search=payload.use_web_search,
                    web_search_topic=payload.web_search_topic,
                    request=request,
                )

                async for chunk in response_stream:
                    yield chunk

                    try:
                        if chunk.startswith("data: "):
                            data_str = chunk[6:].strip()
                            if data_str:
                                data = json.loads(data_str)

                                # Handle v2 streaming format
                                if isinstance(data, dict):
                                    stream_type = data.get("type")
                                    stream_data = data.get("data")

                                    # LLM token streaming (new)
                                    if stream_type == "token":
                                        # Tokens are already streamed to client
                                        # Accumulate for final response if needed
                                        pass

                                    # State updates from nodes
                                    elif stream_type == "updates" and isinstance(
                                        stream_data, dict
                                    ):
                                        # Extract retrieved docs from rerank node
                                        if "rerank_docs_node" in stream_data:
                                            node_data = stream_data["rerank_docs_node"]
                                            if "retrieved_docs" in node_data:
                                                # Convert dict back to Document objects
                                                retrieved_docs = [
                                                    Document(
                                                        page_content=doc.get(
                                                            "page_content", ""
                                                        ),
                                                        metadata=doc.get(
                                                            "metadata", {}
                                                        ),
                                                    )
                                                    for doc in node_data.get(
                                                        "retrieved_docs", []
                                                    )
                                                    if isinstance(doc, dict)
                                                ]

                                        # Extract web search results from web crawl node
                                        if "web_crawl_node" in stream_data:
                                            node_data = stream_data["web_crawl_node"]
                                            if "web_search_results" in node_data:
                                                # Convert dict back to Document objects
                                                web_search_results = [
                                                    Document(
                                                        page_content=doc.get(
                                                            "page_content", ""
                                                        ),
                                                        metadata=doc.get(
                                                            "metadata", {}
                                                        ),
                                                    )
                                                    for doc in node_data.get(
                                                        "web_search_results", []
                                                    )
                                                    if isinstance(doc, dict)
                                                ]

                                        # Extract final response
                                        if "generate_response_node" in stream_data:
                                            node_data = stream_data[
                                                "generate_response_node"
                                            ]
                                            if "response" in node_data:
                                                final_response = node_data["response"]
                                            if "response_metadata" in node_data:
                                                response_metadata = node_data[
                                                    "response_metadata"
                                                ]

                                    # Custom status messages
                                    elif stream_type == "custom":
                                        # Custom events are already streamed to client
                                        pass

                    except (json.JSONDecodeError, KeyError) as e:
                        logger.debug(
                            f"Could not parse chunk for response extraction: {e}"
                        )
                        continue

                if final_response:
                    try:
                        # Save the message first
                        assistant_message_payload = MessageCreate(
                            session_id=payload.session_id,
                            user_id=user_id,
                            content=[
                                {"role": "user", "content": payload.query},
                                {"role": "assistant", "content": final_response},
                            ],
                            parent_message_id=None,
                            model_used=payload.model_name,
                            generation_metadata=response_metadata,
                        )
                        assistant_message = await message_controller.create_message(
                            user_id, assistant_message_payload
                        )
                        logger.info(
                            f"Saved assistant message with id: {assistant_message.id}"
                        )

                        # Prepare and save sources
                        sources_to_create: List[SourceCreate] = []

                        # Add retrieved document sources
                        for doc in retrieved_docs:
                            # Use all available metadata from the document
                            doc_metadata = dict(doc.metadata) if doc.metadata else None

                            sources_to_create.append(
                                SourceCreate(
                                    message_id=assistant_message.id,
                                    source_text=doc.page_content[
                                        :5000
                                    ],  # Limit text length
                                    source_type="document",
                                    source_url=doc.metadata.get("source", ""),
                                    metadata=doc_metadata,
                                )
                            )

                        # Add web search result sources
                        for doc in web_search_results:
                            # Use all available metadata from web documents
                            web_metadata = dict(doc.metadata) if doc.metadata else None

                            sources_to_create.append(
                                SourceCreate(
                                    message_id=assistant_message.id,
                                    source_text=doc.page_content[
                                        :5000
                                    ],  # Limit text length
                                    source_type="web",
                                    source_url=doc.metadata.get("url", ""),
                                    metadata=web_metadata,
                                )
                            )

                        # Save all sources in bulk
                        if sources_to_create:
                            saved_sources = await source_controller.create_sources(
                                sources_to_create
                            )
                            logger.info(
                                f"Saved {len(saved_sources)} sources for message {assistant_message.id}"
                            )

                    except Exception as e:
                        logger.error(
                            f"Failed to save assistant message or sources: {e}"
                        )

            except Exception as e:
                logger.error(f"Error in stream_and_save: {e}")
                yield f"data: {json.dumps({'error': str(e)})}\n\n"

        return StreamingResponse(
            stream_and_save(),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
                "X-Accel-Buffering": "no",
            },
        )

    except Exception as e:
        logger.exception(f"Error querying paper: {e}")
        raise HTTPException(status_code=500, detail=f"Error processing query: {str(e)}")