feat(analyst): Implement data analysis agent with authentication and query handling
- Introduced `analyst_auth_adapter.py` for managing authentication context and access control for the data analysis agent. - Added new API endpoints in `analyst.py` for chat, dashboard, asset management, and metrics, utilizing the new authentication context. - Created Pydantic models in `analyst_schemas.py` for request and response structures, ensuring consistent data handling. - Updated SQL guard logic in `sql_guard.py` to enforce access restrictions based on user roles and contexts. - Implemented migration scripts for new database tables related to the data analysis agent, enhancing data management capabilities. - Removed legacy authentication code from `auth.py`, streamlining the authentication process. This update significantly enhances the data analysis capabilities, providing a robust framework for querying and managing data securely.
This commit is contained in:
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { AppLayout } from './layouts/AppLayout'
|
||||
import { LoginPage } from './pages/login/LoginPage'
|
||||
import { PlaceholderPage } from './pages/PlaceholderPage'
|
||||
import { AnalystQueryPage } from './pages/analytics/AnalystQueryPage'
|
||||
import { loadAuth } from './stores/authStore'
|
||||
import { AgentBanner } from './components/AgentBanner'
|
||||
import { CustomerWealthDashboard } from './pages/dashboard/CustomerWealthDashboard'
|
||||
@@ -87,12 +87,12 @@ export default function AppRoutes() {
|
||||
/>
|
||||
|
||||
<Route path="analyst/home" element={<AnalystMarketDashboard />} />
|
||||
<Route path="analytics/query" element={<PlaceholderPage title="数据分析 · 问数" description="NL2SQL 问数下一迭代;可先使用分析对话。" />} />
|
||||
<Route path="analytics/query" element={<AnalystQueryPage />} />
|
||||
<Route
|
||||
path="analytics/chat"
|
||||
element={
|
||||
<div className="space-y-4">
|
||||
<AgentBanner message="数据分析 Agent:非 analyst 角色查表明细可能返回 403,以 JWT 权限为准。" />
|
||||
<AgentBanner message="轻量对话占位;NL2SQL 查数(表格+SQL)请使用「问数工作台」。" />
|
||||
<AnalystChatShell />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { apiFetch } from './client'
|
||||
|
||||
export type AnalystTable = {
|
||||
columns: string[]
|
||||
rows: unknown[][]
|
||||
}
|
||||
|
||||
export type AnalystMeta = {
|
||||
exec_ms: number
|
||||
row_count: number
|
||||
cache_hit: boolean
|
||||
data_as_of?: string
|
||||
source?: string
|
||||
cost_est?: number
|
||||
}
|
||||
|
||||
export type AnalystChatResponse = {
|
||||
answer: string
|
||||
table: AnalystTable
|
||||
sql: string
|
||||
meta: AnalystMeta
|
||||
disclaimer: string
|
||||
status: string
|
||||
error_code?: string | null
|
||||
suggestions?: string[] | null
|
||||
trace_id?: string | null
|
||||
}
|
||||
|
||||
export async function postAnalystChat(
|
||||
token: string,
|
||||
question: string,
|
||||
sessionId?: string,
|
||||
): Promise<AnalystChatResponse> {
|
||||
const { data } = await apiFetch<AnalystChatResponse>('/api/analyst/chat', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: JSON.stringify({ question, session_id: sessionId ?? null }),
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Alert, Button, Card, Collapse, Input, Space, Spin, Table, Typography } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useState } from 'react'
|
||||
import { postAnalystChat, type AnalystChatResponse } from '../../api/analyst'
|
||||
import { ApiError } from '../../api/client'
|
||||
import { ApiErrorResult } from '../../components/ApiErrorResult'
|
||||
import { PageShell } from '../../components/PageShell'
|
||||
import { useAppAuth } from '../../layouts/AppLayout'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
type RowRecord = Record<string, unknown> & { key: number }
|
||||
|
||||
export function AnalystQueryPage() {
|
||||
const auth = useAppAuth()
|
||||
const [question, setQuestion] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<ApiError | Error | null>(null)
|
||||
const [result, setResult] = useState<AnalystChatResponse | null>(null)
|
||||
|
||||
async function onAsk() {
|
||||
const q = question.trim()
|
||||
if (!q) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const resp = await postAnalystChat(auth.accessToken, q)
|
||||
setResult(resp)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error(String(e)))
|
||||
setResult(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RowRecord> =
|
||||
result?.table.columns.map((col: string, i: number) => ({
|
||||
title: col,
|
||||
key: `${col}-${i}`,
|
||||
render: (_: unknown, record: RowRecord) => String(record[`c${i}`] ?? ''),
|
||||
})) ?? []
|
||||
|
||||
const dataSource: RowRecord[] =
|
||||
result?.table.rows.map((row: unknown[], idx: number) => {
|
||||
const record: RowRecord = { key: idx }
|
||||
row.forEach((cell: unknown, i: number) => {
|
||||
record[`c${i}`] = cell
|
||||
})
|
||||
return record
|
||||
}) ?? []
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title="数据分析 · 问数工作台"
|
||||
alert={
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="复杂查数走本页;轻量对话请用「分析对话」。客户侧仅趋势与统计解读,不含投资建议。"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
placeholder="例如:我近30天有多少笔交易? / 待处理预警按类型统计"
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault()
|
||||
void onAsk()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" loading={loading} onClick={() => void onAsk()}>
|
||||
问数
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{error ? <ApiErrorResult error={error} /> : null}
|
||||
|
||||
{loading ? <Spin tip="生成 SQL 并查询中…" /> : null}
|
||||
|
||||
{result ? (
|
||||
<>
|
||||
{result.status === 'clarify' ? <Alert type="warning" message={result.answer} /> : null}
|
||||
{result.status === 'deny' ? (
|
||||
<Alert
|
||||
type="error"
|
||||
message={result.answer}
|
||||
description={result.suggestions?.join(' · ')}
|
||||
/>
|
||||
) : null}
|
||||
{(result.status === 'success' || result.status === 'degrade') && result.answer ? (
|
||||
<Card title="解读" size="small">
|
||||
<Paragraph>{result.answer}</Paragraph>
|
||||
{result.disclaimer ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{result.disclaimer}
|
||||
</Text>
|
||||
) : null}
|
||||
</Card>
|
||||
) : null}
|
||||
{result.table.rows.length > 0 ? (
|
||||
<Card title="结果表格" size="small">
|
||||
<Table<RowRecord>
|
||||
size="small"
|
||||
rowKey="key"
|
||||
pagination={{ pageSize: 10 }}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: true }}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
{result.sql ? (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'sql',
|
||||
label: `SQL(${result.meta.row_count} 行 · ${result.meta.exec_ms} ms${
|
||||
result.meta.cache_hit ? ' · 缓存命中' : ''
|
||||
})`,
|
||||
children: <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{result.sql}</pre>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
</PageShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user