Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
api/routes/query.py
CHANGED
|
@@ -62,7 +62,6 @@ async def stream_query_policy(body: QueryRequest, request: Request):
|
|
| 62 |
yield token
|
| 63 |
except Exception as exc:
|
| 64 |
logger.exception("QueryService.stream_query failed")
|
| 65 |
-
|
| 66 |
-
yield f"data: {json.dumps({'type': 'error', 'content': str(exc)})}\n\n"
|
| 67 |
|
| 68 |
-
return StreamingResponse(content_generator(), media_type="text/
|
|
|
|
| 62 |
yield token
|
| 63 |
except Exception as exc:
|
| 64 |
logger.exception("QueryService.stream_query failed")
|
| 65 |
+
yield f"Error: {str(exc)}"
|
|
|
|
| 66 |
|
| 67 |
+
return StreamingResponse(content_generator(), media_type="text/plain")
|
backend/src/routes/query.js
CHANGED
|
@@ -40,37 +40,12 @@ router.post('/query/stream', authMiddleware, async (req, res) => {
|
|
| 40 |
question, policy_id, k
|
| 41 |
}, { responseType: 'stream' });
|
| 42 |
|
| 43 |
-
res.setHeader('Content-Type', 'text/
|
| 44 |
-
res.setHeader('Cache-Control', 'no-cache');
|
| 45 |
-
res.setHeader('Connection', 'keep-alive');
|
| 46 |
|
| 47 |
-
let
|
| 48 |
-
let sourcesData = null;
|
| 49 |
-
let buffer = "";
|
| 50 |
-
|
| 51 |
response.data.on('data', (chunk) => {
|
|
|
|
| 52 |
res.write(chunk);
|
| 53 |
-
|
| 54 |
-
buffer += chunk.toString();
|
| 55 |
-
const lines = buffer.split('\n\n');
|
| 56 |
-
buffer = lines.pop() || '';
|
| 57 |
-
|
| 58 |
-
for (const line of lines) {
|
| 59 |
-
if (line.startsWith('data: ')) {
|
| 60 |
-
const jsonStr = line.slice(6).trim();
|
| 61 |
-
if (jsonStr === '[DONE]') continue;
|
| 62 |
-
try {
|
| 63 |
-
const parsed = JSON.parse(jsonStr);
|
| 64 |
-
if (parsed.type === 'token') {
|
| 65 |
-
answerText += parsed.content;
|
| 66 |
-
} else if (parsed.type === 'sources') {
|
| 67 |
-
sourcesData = parsed.sources;
|
| 68 |
-
}
|
| 69 |
-
} catch (e) {
|
| 70 |
-
// ignore partial JSON parse errors
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
}
|
| 74 |
});
|
| 75 |
|
| 76 |
response.data.on('end', async () => {
|
|
@@ -80,8 +55,7 @@ router.post('/query/stream', authMiddleware, async (req, res) => {
|
|
| 80 |
user_id: req.user?.id || 'anonymous',
|
| 81 |
policy_id,
|
| 82 |
question,
|
| 83 |
-
answer:
|
| 84 |
-
sources: sourcesData
|
| 85 |
});
|
| 86 |
} catch (dbErr) {
|
| 87 |
console.error('[query/stream] DB Save Error:', dbErr.message);
|
|
|
|
| 40 |
question, policy_id, k
|
| 41 |
}, { responseType: 'stream' });
|
| 42 |
|
| 43 |
+
res.setHeader('Content-Type', 'text/plain');
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
let fullAnswer = "";
|
|
|
|
|
|
|
|
|
|
| 46 |
response.data.on('data', (chunk) => {
|
| 47 |
+
fullAnswer += chunk.toString();
|
| 48 |
res.write(chunk);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
});
|
| 50 |
|
| 51 |
response.data.on('end', async () => {
|
|
|
|
| 55 |
user_id: req.user?.id || 'anonymous',
|
| 56 |
policy_id,
|
| 57 |
question,
|
| 58 |
+
answer: fullAnswer
|
|
|
|
| 59 |
});
|
| 60 |
} catch (dbErr) {
|
| 61 |
console.error('[query/stream] DB Save Error:', dbErr.message);
|
frontend/src/api.js
CHANGED
|
@@ -61,7 +61,7 @@ export const fetchApi = async (endpoint, options = {}) => {
|
|
| 61 |
}
|
| 62 |
};
|
| 63 |
|
| 64 |
-
export const streamApi = async (endpoint, options = {},
|
| 65 |
const url = `${API_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
| 66 |
const fetchOptions = {
|
| 67 |
...options,
|
|
@@ -81,28 +81,13 @@ export const streamApi = async (endpoint, options = {}, onEvent) => {
|
|
| 81 |
const reader = response.body.getReader();
|
| 82 |
const decoder = new TextDecoder();
|
| 83 |
let done = false;
|
| 84 |
-
let buffer = '';
|
| 85 |
|
| 86 |
while (!done) {
|
| 87 |
const { value, done: doneReading } = await reader.read();
|
| 88 |
done = doneReading;
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
buffer = lines.pop() || ''; // Keep the last incomplete chunk
|
| 93 |
-
|
| 94 |
-
for (const line of lines) {
|
| 95 |
-
if (line.startsWith('data: ')) {
|
| 96 |
-
const jsonStr = line.slice(6).trim();
|
| 97 |
-
if (jsonStr === '[DONE]') continue;
|
| 98 |
-
try {
|
| 99 |
-
const parsed = JSON.parse(jsonStr);
|
| 100 |
-
onEvent(parsed);
|
| 101 |
-
} catch (e) {
|
| 102 |
-
console.error('Failed to parse SSE JSON:', jsonStr, e);
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
}
|
| 107 |
}
|
| 108 |
};
|
|
|
|
| 61 |
}
|
| 62 |
};
|
| 63 |
|
| 64 |
+
export const streamApi = async (endpoint, options = {}, onToken) => {
|
| 65 |
const url = `${API_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
| 66 |
const fetchOptions = {
|
| 67 |
...options,
|
|
|
|
| 81 |
const reader = response.body.getReader();
|
| 82 |
const decoder = new TextDecoder();
|
| 83 |
let done = false;
|
|
|
|
| 84 |
|
| 85 |
while (!done) {
|
| 86 |
const { value, done: doneReading } = await reader.read();
|
| 87 |
done = doneReading;
|
| 88 |
+
const chunkValue = decoder.decode(value);
|
| 89 |
+
if (chunkValue) {
|
| 90 |
+
onToken(chunkValue);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
};
|
frontend/src/components/Dashboard/Chatbot.jsx
CHANGED
|
@@ -94,19 +94,8 @@ function MessageBubble({ msg, T }) {
|
|
| 94 |
<Aperture size={18} style={{ color:T.acc }}/>
|
| 95 |
</div>
|
| 96 |
)}
|
| 97 |
-
<div style={{ maxWidth:'75%',
|
| 98 |
-
|
| 99 |
-
{msg.text}
|
| 100 |
-
</div>
|
| 101 |
-
{!isUser && msg.sources && msg.sources.length > 0 && (
|
| 102 |
-
<div style={{ display:'flex', flexDirection:'column', gap:4, marginTop:4, alignSelf:'flex-start' }}>
|
| 103 |
-
{msg.sources.map((s, idx) => (
|
| 104 |
-
<div key={idx} style={{ padding:'6px 10px', background:T.msgAiBg, border:`1px solid ${T.cardBorder}`, borderRadius:8, fontSize:11, fontFamily:"'JetBrains Mono', monospace", color:T.acc, lineHeight:1.3 }}>
|
| 105 |
-
<span style={{ fontWeight:600 }}>Source:</span> {s.section !== 'Unknown' ? s.section : (s.clause_type !== 'Unknown' ? s.clause_type : 'Document')} {s.page_number ? `(Page ${s.page_number})` : ''}
|
| 106 |
-
</div>
|
| 107 |
-
))}
|
| 108 |
-
</div>
|
| 109 |
-
)}
|
| 110 |
</div>
|
| 111 |
{isUser && (
|
| 112 |
<div style={{ width:36, height:36, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
|
|
@@ -156,18 +145,10 @@ export default function Chatbot({ file, isDark: initDark }) {
|
|
| 156 |
await streamApi('/query/stream', {
|
| 157 |
method: 'POST',
|
| 158 |
body: { question: text, policy_id: file?.policy_id || 'test' }
|
| 159 |
-
}, (
|
| 160 |
-
setMessages(prev => prev.map(m =>
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
return { ...m, text: m.text + event.content };
|
| 164 |
-
} else if (event.type === 'sources') {
|
| 165 |
-
return { ...m, sources: event.sources };
|
| 166 |
-
} else if (event.type === 'error') {
|
| 167 |
-
return { ...m, text: m.text + '\n\n[Error]: ' + event.content };
|
| 168 |
-
}
|
| 169 |
-
return m;
|
| 170 |
-
}));
|
| 171 |
});
|
| 172 |
} catch (err) {
|
| 173 |
setMessages(p => [...p, { id: crypto.randomUUID(), sender: 'ai', text: 'Error: ' + err.message }]);
|
|
|
|
| 94 |
<Aperture size={18} style={{ color:T.acc }}/>
|
| 95 |
</div>
|
| 96 |
)}
|
| 97 |
+
<div style={{ maxWidth:'75%', padding:'14px 20px', fontSize:15, lineHeight:1.6, borderRadius: isUser ? '20px 4px 20px 20px' : '4px 20px 20px 20px', background: isUser ? T.msgUserBg : T.msgAiBg, color: isUser ? T.msgUserText : T.msgAiText, boxShadow: isUser ? T.msgUserShadow : 'none', fontFamily:"'DM Sans', sans-serif", fontWeight:400, transition:'background .4s, color .4s' }}>
|
| 98 |
+
{msg.text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
</div>
|
| 100 |
{isUser && (
|
| 101 |
<div style={{ width:36, height:36, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
|
|
|
|
| 145 |
await streamApi('/query/stream', {
|
| 146 |
method: 'POST',
|
| 147 |
body: { question: text, policy_id: file?.policy_id || 'test' }
|
| 148 |
+
}, (token) => {
|
| 149 |
+
setMessages(prev => prev.map(m =>
|
| 150 |
+
m.id === aiMsgId ? { ...m, text: m.text + token } : m
|
| 151 |
+
));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
});
|
| 153 |
} catch (err) {
|
| 154 |
setMessages(p => [...p, { id: crypto.randomUUID(), sender: 'ai', text: 'Error: ' + err.message }]);
|
frontend/src/components/Dashboard/Dboard.jsx
CHANGED
|
@@ -475,20 +475,12 @@ export default function Dboard({ file, isDark: _initDark, userName = 'My Account
|
|
| 475 |
...prev,
|
| 476 |
[activePolicyId]: [...(prev[activePolicyId] ?? []), { id:aiMsgId, sender:'ai', text:'' }],
|
| 477 |
}));
|
| 478 |
-
await streamApi('/query/stream', { method:'POST', body:{ question:text, policy_id:pid } }, (
|
| 479 |
setChatMessagesMap(prev => ({
|
| 480 |
...prev,
|
| 481 |
-
[activePolicyId]: prev[activePolicyId].map(m =>
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
return { ...m, text: m.text + event.content };
|
| 485 |
-
} else if (event.type === 'sources') {
|
| 486 |
-
return { ...m, sources: event.sources };
|
| 487 |
-
} else if (event.type === 'error') {
|
| 488 |
-
return { ...m, text: m.text + '\n\n[Error]: ' + event.content };
|
| 489 |
-
}
|
| 490 |
-
return m;
|
| 491 |
-
}),
|
| 492 |
}));
|
| 493 |
});
|
| 494 |
} catch (err) {
|
|
@@ -1014,19 +1006,8 @@ export default function Dboard({ file, isDark: _initDark, userName = 'My Account
|
|
| 1014 |
<Aperture size={15} style={{ color:T.acc }} />
|
| 1015 |
</div>
|
| 1016 |
)}
|
| 1017 |
-
<div style={{ maxWidth:'80%',
|
| 1018 |
-
|
| 1019 |
-
{msg.text}
|
| 1020 |
-
</div>
|
| 1021 |
-
{!isUser && msg.sources && msg.sources.length > 0 && (
|
| 1022 |
-
<div style={{ display:'flex', flexDirection:'column', gap:4, marginTop:2, alignSelf:'flex-start' }}>
|
| 1023 |
-
{msg.sources.map((s, idx) => (
|
| 1024 |
-
<div key={idx} style={{ padding:'6px 10px', background:T.badgeBg, border:`1px solid ${T.navActiveBrd}`, borderRadius:8, fontSize:10, ...mono, color:T.acc, lineHeight:1.3 }}>
|
| 1025 |
-
<span style={{ fontWeight:600 }}>Source:</span> {s.section !== 'Unknown' ? s.section : (s.clause_type !== 'Unknown' ? s.clause_type : 'Document')} {s.page_number ? `(Page ${s.page_number})` : ''}
|
| 1026 |
-
</div>
|
| 1027 |
-
))}
|
| 1028 |
-
</div>
|
| 1029 |
-
)}
|
| 1030 |
</div>
|
| 1031 |
</div>
|
| 1032 |
);
|
|
|
|
| 475 |
...prev,
|
| 476 |
[activePolicyId]: [...(prev[activePolicyId] ?? []), { id:aiMsgId, sender:'ai', text:'' }],
|
| 477 |
}));
|
| 478 |
+
await streamApi('/query/stream', { method:'POST', body:{ question:text, policy_id:pid } }, (token) => {
|
| 479 |
setChatMessagesMap(prev => ({
|
| 480 |
...prev,
|
| 481 |
+
[activePolicyId]: prev[activePolicyId].map(m =>
|
| 482 |
+
m.id === aiMsgId ? { ...m, text: m.text + token } : m
|
| 483 |
+
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
}));
|
| 485 |
});
|
| 486 |
} catch (err) {
|
|
|
|
| 1006 |
<Aperture size={15} style={{ color:T.acc }} />
|
| 1007 |
</div>
|
| 1008 |
)}
|
| 1009 |
+
<div style={{ maxWidth:'80%', padding:'12px 16px', fontSize:13, lineHeight:1.6, borderRadius: isUser ? '18px 4px 18px 18px' : '4px 18px 18px 18px', background: isUser ? T.msgUserBg : T.msgAiBg, color: isUser ? T.msgUserText : T.msgAiText, boxShadow: isUser ? T.msgUserShadow : 'none', ...f, transition:'background .4s, color .4s' }}>
|
| 1010 |
+
{msg.text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1011 |
</div>
|
| 1012 |
</div>
|
| 1013 |
);
|
rag_engine/services/query_service.py
CHANGED
|
@@ -101,41 +101,9 @@ class QueryService:
|
|
| 101 |
|
| 102 |
prompt = build_query_prompt(question, context, policy_id)
|
| 103 |
|
| 104 |
-
import json
|
| 105 |
-
import time
|
| 106 |
# Step 5 — LLM Stream
|
| 107 |
for token in self._llm.stream(prompt, system=SYSTEM_PROMPT):
|
| 108 |
-
|
| 109 |
-
chunk_size = 2
|
| 110 |
-
for i in range(0, len(token), chunk_size):
|
| 111 |
-
piece = token[i:i+chunk_size]
|
| 112 |
-
yield f"data: {json.dumps({'type': 'token', 'content': piece})}\n\n"
|
| 113 |
-
time.sleep(0.015)
|
| 114 |
-
|
| 115 |
-
# Step 6 — Yield sources at the end
|
| 116 |
-
seen_sources = set()
|
| 117 |
-
unique_sources = []
|
| 118 |
-
for chunk in reranked[:5]:
|
| 119 |
-
sec = chunk.get("metadata", {}).get("section_name", "Unknown")
|
| 120 |
-
page = chunk.get("metadata", {}).get("page_number")
|
| 121 |
-
key = (sec, page)
|
| 122 |
-
if key not in seen_sources:
|
| 123 |
-
seen_sources.add(key)
|
| 124 |
-
unique_sources.append({
|
| 125 |
-
"section": sec,
|
| 126 |
-
"clause_type": chunk.get("metadata", {}).get("clause_type", "Unknown"),
|
| 127 |
-
"page_number": page,
|
| 128 |
-
"highlight_text": chunk.get("content", ""),
|
| 129 |
-
"relevance_score": round(chunk.get("rerank_score", chunk.get("score", 0)), 4),
|
| 130 |
-
"snippet": (
|
| 131 |
-
chunk.get("content", "")[:200] + "..."
|
| 132 |
-
if len(chunk.get("content", "")) > 200
|
| 133 |
-
else chunk.get("content", "")
|
| 134 |
-
),
|
| 135 |
-
})
|
| 136 |
-
|
| 137 |
-
yield f"data: {json.dumps({'type': 'sources', 'sources': unique_sources})}\n\n"
|
| 138 |
-
yield "data: [DONE]\n\n"
|
| 139 |
|
| 140 |
# ------------------------------------------------------------------ #
|
| 141 |
# multi-policy query
|
|
|
|
| 101 |
|
| 102 |
prompt = build_query_prompt(question, context, policy_id)
|
| 103 |
|
|
|
|
|
|
|
| 104 |
# Step 5 — LLM Stream
|
| 105 |
for token in self._llm.stream(prompt, system=SYSTEM_PROMPT):
|
| 106 |
+
yield token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
# ------------------------------------------------------------------ #
|
| 109 |
# multi-policy query
|