data source binded
This commit is contained in:
+20
-3
@@ -1,5 +1,6 @@
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -84,6 +85,13 @@ class ChatRequest(BaseModel):
|
|||||||
file_url: Optional[str] = None
|
file_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _should_use_nl2sql(request: ChatRequest) -> bool:
|
||||||
|
if request.prefer_sql_chart:
|
||||||
|
return True
|
||||||
|
source = (request.source or "").strip().lower()
|
||||||
|
return source == "upload" or source.startswith("ds:")
|
||||||
|
|
||||||
|
|
||||||
class SessionAliasUpdateRequest(BaseModel):
|
class SessionAliasUpdateRequest(BaseModel):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
pinned: Optional[bool] = None
|
pinned: Optional[bool] = None
|
||||||
@@ -96,6 +104,7 @@ class BatchDeleteRequest(BaseModel):
|
|||||||
|
|
||||||
class SessionFileContextUpdateRequest(BaseModel):
|
class SessionFileContextUpdateRequest(BaseModel):
|
||||||
active_data_file: Optional[Dict[str, Any]] = None
|
active_data_file: Optional[Dict[str, Any]] = None
|
||||||
|
selected_data_source: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def _build_sql_chart_text(nl2sql_result: NL2SQLResponse) -> str:
|
def _build_sql_chart_text(nl2sql_result: NL2SQLResponse) -> str:
|
||||||
@@ -112,12 +121,13 @@ def _build_sql_chart_text(nl2sql_result: NL2SQLResponse) -> str:
|
|||||||
|
|
||||||
def _build_sql_chart_viz(nl2sql_result: NL2SQLResponse) -> dict:
|
def _build_sql_chart_viz(nl2sql_result: NL2SQLResponse) -> dict:
|
||||||
chart = nl2sql_result.chart
|
chart = nl2sql_result.chart
|
||||||
return {
|
payload = {
|
||||||
"sql": nl2sql_result.sql,
|
"sql": nl2sql_result.sql,
|
||||||
"result": nl2sql_result.result,
|
"result": nl2sql_result.result,
|
||||||
"chart": chart.model_dump() if chart else None,
|
"chart": chart.model_dump() if chart else None,
|
||||||
"error": nl2sql_result.error,
|
"error": nl2sql_result.error,
|
||||||
}
|
}
|
||||||
|
return jsonable_encoder(payload)
|
||||||
|
|
||||||
|
|
||||||
def _persist_session_turn(
|
def _persist_session_turn(
|
||||||
@@ -136,7 +146,7 @@ def _persist_session_turn(
|
|||||||
@app.post("/nanobot/chat")
|
@app.post("/nanobot/chat")
|
||||||
async def nanobot_chat(request: ChatRequest):
|
async def nanobot_chat(request: ChatRequest):
|
||||||
try:
|
try:
|
||||||
if request.prefer_sql_chart:
|
if _should_use_nl2sql(request):
|
||||||
nl2sql_result = await process_nl2sql(
|
nl2sql_result = await process_nl2sql(
|
||||||
NL2SQLRequest(query=request.message, source=request.source, file_url=request.file_url)
|
NL2SQLRequest(query=request.message, source=request.source, file_url=request.file_url)
|
||||||
)
|
)
|
||||||
@@ -161,7 +171,7 @@ async def nanobot_chat(request: ChatRequest):
|
|||||||
async def nanobot_chat_stream(request: ChatRequest):
|
async def nanobot_chat_stream(request: ChatRequest):
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
try:
|
try:
|
||||||
if request.prefer_sql_chart:
|
if _should_use_nl2sql(request):
|
||||||
nl2sql_result = await process_nl2sql(
|
nl2sql_result = await process_nl2sql(
|
||||||
NL2SQLRequest(query=request.message, source=request.source, file_url=request.file_url)
|
NL2SQLRequest(query=request.message, source=request.source, file_url=request.file_url)
|
||||||
)
|
)
|
||||||
@@ -295,10 +305,17 @@ def update_session_context_file(session_id: str, payload: SessionFileContextUpda
|
|||||||
if not nanobot_service.agent:
|
if not nanobot_service.agent:
|
||||||
raise HTTPException(status_code=400, detail="Nanobot not running")
|
raise HTTPException(status_code=400, detail="Nanobot not running")
|
||||||
session = nanobot_service.agent.sessions.get_or_create(session_id)
|
session = nanobot_service.agent.sessions.get_or_create(session_id)
|
||||||
|
updated_fields = payload.model_fields_set
|
||||||
|
if "active_data_file" in updated_fields:
|
||||||
if payload.active_data_file is None:
|
if payload.active_data_file is None:
|
||||||
session.metadata.pop("active_data_file", None)
|
session.metadata.pop("active_data_file", None)
|
||||||
else:
|
else:
|
||||||
session.metadata["active_data_file"] = payload.active_data_file
|
session.metadata["active_data_file"] = payload.active_data_file
|
||||||
|
if "selected_data_source" in updated_fields:
|
||||||
|
if payload.selected_data_source:
|
||||||
|
session.metadata["selected_data_source"] = payload.selected_data_source
|
||||||
|
else:
|
||||||
|
session.metadata.pop("selected_data_source", None)
|
||||||
session.updated_at = datetime.now()
|
session.updated_at = datetime.now()
|
||||||
nanobot_service.agent.sessions.save(session)
|
nanobot_service.agent.sessions.save(session)
|
||||||
return {"status": "success", "metadata": session.metadata}
|
return {"status": "success", "metadata": session.metadata}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ interface SessionData {
|
|||||||
key: string;
|
key: string;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
active_data_file?: DataFileContext | null;
|
active_data_file?: DataFileContext | null;
|
||||||
|
selected_data_source?: string | null;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
messages: Array<{
|
messages: Array<{
|
||||||
@@ -104,7 +105,6 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
setSelectedDataSource("");
|
|
||||||
fetchDataSources();
|
fetchDataSources();
|
||||||
}
|
}
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
@@ -117,26 +117,37 @@ export function ChatInterface() {
|
|||||||
setAvailableDataSources(projectSources);
|
setAvailableDataSources(projectSources);
|
||||||
if (selectedDataSource && !projectSources.find(ds => ds.id === selectedDataSource)) {
|
if (selectedDataSource && !projectSources.find(ds => ds.id === selectedDataSource)) {
|
||||||
setSelectedDataSource("");
|
setSelectedDataSource("");
|
||||||
|
void syncSessionContext({ selected_data_source: null });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch data sources", e);
|
console.error("Failed to fetch data sources", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncSessionFileContext = async (file: DataFileContext | null) => {
|
const syncSessionContext = async (payload: {
|
||||||
|
active_data_file?: DataFileContext | null;
|
||||||
|
selected_data_source?: string | null;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
await api.put(`/nanobot/sessions/${encodeURIComponent(activeSessionKey)}/context-file`, {
|
await api.put(`/nanobot/sessions/${encodeURIComponent(activeSessionKey)}/context-file`, payload);
|
||||||
active_data_file: file,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to sync session file context", e);
|
console.error("Failed to sync session context", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectDataSource = async (sourceId: string) => {
|
||||||
|
setSelectedDataSource(sourceId);
|
||||||
|
await syncSessionContext({ selected_data_source: sourceId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearDataSource = async () => {
|
||||||
|
setSelectedDataSource("");
|
||||||
|
await syncSessionContext({ selected_data_source: null });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSessionData = async () => {
|
const fetchSessionData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setSelectedDataSource("");
|
|
||||||
setSelectedSkillIds([]);
|
setSelectedSkillIds([]);
|
||||||
try {
|
try {
|
||||||
const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`);
|
const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`);
|
||||||
@@ -152,12 +163,15 @@ export function ChatInterface() {
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
}
|
}
|
||||||
const restoredFile = data.metadata?.active_data_file || null;
|
const restoredFile = data.metadata?.active_data_file || null;
|
||||||
|
const restoredSource = data.metadata?.selected_data_source || "";
|
||||||
setActiveDataFile(restoredFile);
|
setActiveDataFile(restoredFile);
|
||||||
|
setSelectedDataSource(restoredSource);
|
||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch session messages", e);
|
console.error("Failed to fetch session messages", e);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setActiveDataFile(null);
|
setActiveDataFile(null);
|
||||||
|
setSelectedDataSource("");
|
||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -238,7 +252,7 @@ export function ChatInterface() {
|
|||||||
setAttachedFile(uploadedFile);
|
setAttachedFile(uploadedFile);
|
||||||
setActiveDataFile(uploadedFile);
|
setActiveDataFile(uploadedFile);
|
||||||
setSelectedDataSource("");
|
setSelectedDataSource("");
|
||||||
await syncSessionFileContext(uploadedFile);
|
await syncSessionContext({ active_data_file: uploadedFile, selected_data_source: null });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("File upload error:", error);
|
console.error("File upload error:", error);
|
||||||
// Could show a toast notification here
|
// Could show a toast notification here
|
||||||
@@ -253,7 +267,7 @@ export function ChatInterface() {
|
|||||||
const handleRemoveFile = async () => {
|
const handleRemoveFile = async () => {
|
||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
setActiveDataFile(null);
|
setActiveDataFile(null);
|
||||||
await syncSessionFileContext(null);
|
await syncSessionContext({ active_data_file: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedDataSourceName = availableDataSources.find(ds => ds.id === selectedDataSource)?.name || "";
|
const selectedDataSourceName = availableDataSources.find(ds => ds.id === selectedDataSource)?.name || "";
|
||||||
@@ -610,7 +624,7 @@ export function ChatInterface() {
|
|||||||
<button
|
<button
|
||||||
key={ds.id}
|
key={ds.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDataSource(ds.id);
|
void handleSelectDataSource(ds.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
||||||
@@ -629,7 +643,9 @@ export function ChatInterface() {
|
|||||||
{selectedDataSource && (
|
{selectedDataSource && (
|
||||||
<div className="mt-2 pt-2 border-t border-zinc-100">
|
<div className="mt-2 pt-2 border-t border-zinc-100">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedDataSource("")}
|
onClick={() => {
|
||||||
|
void handleClearDataSource();
|
||||||
|
}}
|
||||||
className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1"
|
className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1"
|
||||||
>
|
>
|
||||||
清除已选
|
清除已选
|
||||||
@@ -814,7 +830,7 @@ export function ChatInterface() {
|
|||||||
<button
|
<button
|
||||||
key={ds.id}
|
key={ds.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDataSource(ds.id);
|
void handleSelectDataSource(ds.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
||||||
@@ -833,7 +849,9 @@ export function ChatInterface() {
|
|||||||
{selectedDataSource && (
|
{selectedDataSource && (
|
||||||
<div className="mt-2 pt-2 border-t border-zinc-100">
|
<div className="mt-2 pt-2 border-t border-zinc-100">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedDataSource("")}
|
onClick={() => {
|
||||||
|
void handleClearDataSource();
|
||||||
|
}}
|
||||||
className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1"
|
className="w-full py-1.5 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors flex items-center justify-center gap-1"
|
||||||
>
|
>
|
||||||
清除已选
|
清除已选
|
||||||
|
|||||||
Reference in New Issue
Block a user