Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PORT=3002
NODE_ENV=development
SQLITE_PATH=../../data/sqlite/text2sql.db
SQLITE_ALLOWED_DIRS=../../data/sqlite,../../data/uploads/datasources
CORS_ALLOWED_ORIGINS=http://localhost:3000
DATASOURCE_UPLOAD_DIR=../../data/uploads/datasources
DATASOURCE_UPLOAD_MAX_BYTES=10485760
Expand All @@ -23,9 +24,10 @@ LLM_MODEL=doubao-1.5-pro-32k
LLM_API_KEY=
LLM_BASE_URL=
LLM_TIMEOUT_MS=30000
LLM_STREAM_TIMEOUT_MS=90000
LLM_MOCK_MODE=false
LLM_FALLBACK_PROVIDERS=siliconflow,minimax
AGENT_PLANNING_SCAFFOLD_ENABLED=false
AGENT_PLANNING_SCAFFOLD_ENABLED=true

# Clarification hybrid gate defaults:
# - enabled by default
Expand Down
8 changes: 6 additions & 2 deletions apps/backend/src/modules/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ export class ChatController {
const run = await this.chatService.sendMessage(
sessionId,
body.message,
req.requestId
req.requestId,
undefined,
req.actor
);
return ok(req.requestId, {
kind: "agent-run",
Expand Down Expand Up @@ -195,7 +197,9 @@ export class ChatController {
req.requestId,
async (event) => {
sendEvent(event.type, event);
}
},
undefined,
req.actor
);
} catch (error) {
const fallbackEvent: ChatStreamEvent = {
Expand Down
58 changes: 41 additions & 17 deletions apps/backend/src/modules/config/app-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@ export class AppConfigService {

get sqlitePath(): string {
const raw = this.config.get<string>("SQLITE_PATH", "data/sqlite/text2sql.db");
if (isAbsolute(raw)) {
return raw;
}
const direct = resolve(process.cwd(), raw);
if (existsSync(direct)) {
return direct;
}
const fallback = resolve(process.cwd(), "../../", raw);
return fallback;
return this.resolveConfiguredPath(raw);
}

get sqliteAllowedDirs(): string[] {
const raw = this.config.get<string>(
"SQLITE_ALLOWED_DIRS",
"data/sqlite,data/uploads/datasources"
);
const parsed = raw
.split(",")
.map((item) => item.trim())
.filter(Boolean)
.map((item) => this.resolveConfiguredPath(item));
return Array.from(new Set(parsed));
}

get redisUrl(): string {
Expand All @@ -50,14 +55,7 @@ export class AppConfigService {
"DATASOURCE_UPLOAD_DIR",
"data/uploads/datasources"
);
if (isAbsolute(raw)) {
return raw;
}
const direct = resolve(process.cwd(), raw);
if (existsSync(direct)) {
return direct;
}
return resolve(process.cwd(), "../../", raw);
return this.resolveConfiguredPath(raw);
}

get datasourceUploadMaxBytes(): number {
Expand Down Expand Up @@ -125,6 +123,19 @@ export class AppConfigService {
return Number(this.config.get<string>("LLM_TIMEOUT_MS", "30000"));
}

get llmStreamTimeoutMs(): number {
const fallback = this.llmTimeoutMs;
const raw = this.config.get<string>("LLM_STREAM_TIMEOUT_MS", String(fallback));
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed);
}
this.logger.warn(
`LLM_STREAM_TIMEOUT_MS 配置无效(${raw}),已回退到 LLM_TIMEOUT_MS=${fallback}。`
);
return fallback;
}

get llmMockMode(): boolean {
return this.config.get<string>("LLM_MOCK_MODE", "false") === "true";
}
Expand Down Expand Up @@ -275,4 +286,17 @@ export class AppConfigService {
this.logger.warn(`${key} 配置无效(${raw}),已回退默认值 ${defaultValue}。`);
return defaultValue;
}

private resolveConfiguredPath(raw: string): string {
if (isAbsolute(raw)) {
return resolve(raw);
}

const direct = resolve(process.cwd(), raw);
const fallback = resolve(process.cwd(), "../../", raw);
if (existsSync(direct) || !existsSync(fallback)) {
return direct;
}
return fallback;
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/modules/conversation/agent/agent.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { BuildSemanticQueryNode } from "./nodes/build-semantic-query.node";
import { GraphBuilderService } from "./graph/graph.builder";
import { LangGraphRuntimeService } from "./graph/langgraph.runtime";
import { GenerateSqlNode } from "./nodes/generate-sql.node";
import { ResolveSavedPriorSqlNode } from "./nodes/resolve-saved-prior-sql.node";
import { SafetyCheckNode } from "./nodes/safety-check.node";
import { ExecuteSqlNode } from "./nodes/execute-sql.node";
import { SqlGenerationService } from "./sql/sql-generation.service";
Expand Down Expand Up @@ -70,6 +71,7 @@ import { PlannerCacheService } from "./planner/planner-cache.service";
PlannerCacheService,
BuildSemanticQueryNode,
BuildPhysicalPlanNode,
ResolveSavedPriorSqlNode,
GenerateSqlNode,
SafetyCheckNode,
ExecuteSqlNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const AGENT_STEP_STAGE_MAP: Record<string, ReasoningStage> = {
"build-intent-plan": "analysis",
"build-semantic-query": "analysis",
"build-physical-plan": "analysis",
"resolve-saved-prior-sql": "analysis",
"generate-sql": "generation",
"safety-check": "validation",
"execute-sql": "execution",
Expand All @@ -20,6 +21,7 @@ const AGENT_STEP_TITLE_MAP: Record<string, string> = {
"build-intent-plan": "意图规划",
"build-semantic-query": "语义规划",
"build-physical-plan": "物理规划",
"resolve-saved-prior-sql": "Prior SQL 复用判断",
"generate-sql": "生成 SQL",
"safety-check": "安全校验",
"execute-sql": "执行查询",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ClarifyNode } from "../nodes/clarify.node";
import { ExecuteSqlNode } from "../nodes/execute-sql.node";
import { FormatAnswerNode } from "../nodes/format-answer.node";
import { GenerateSqlNode } from "../nodes/generate-sql.node";
import { ResolveSavedPriorSqlNode } from "../nodes/resolve-saved-prior-sql.node";
import { RetrieveKnowledgeNode } from "../nodes/retrieve-knowledge.node";
import { SafetyCheckNode } from "../nodes/safety-check.node";
import type {
Expand Down Expand Up @@ -198,6 +199,7 @@ export interface LangGraphNodeDependencies {
buildIntentPlanNode: Pick<BuildIntentPlanNode, "run">;
buildSemanticQueryNode: Pick<BuildSemanticQueryNode, "run">;
buildPhysicalPlanNode: Pick<BuildPhysicalPlanNode, "run">;
resolveSavedPriorSqlNode: Pick<ResolveSavedPriorSqlNode, "run">;
generateSqlNode: Pick<GenerateSqlNode, "run">;
safetyNode: Pick<SafetyCheckNode, "run">;
executeNode: Pick<ExecuteSqlNode, "run">;
Expand Down Expand Up @@ -835,6 +837,7 @@ export const createLangGraphNodeHandlers = (deps: LangGraphNodeDependencies) =>
},
sql: generated.sql,
explanation: generated.explanation,
savedPriorSqlFallbackRequested: false,
error: undefined,
fatalError: undefined
};
Expand Down Expand Up @@ -920,6 +923,9 @@ export const createLangGraphNodeHandlers = (deps: LangGraphNodeDependencies) =>
workspaceId: state.accessContext?.workspaceId
};
if (!safety.allowed) {
const shouldFallbackToGeneration =
state.savedPriorSqlResolution?.status === "hit" &&
state.savedPriorSqlResolution.fallbackToGeneration === false;
const trace = appendStep(state, {
step: {
node: "safety-check",
Expand All @@ -939,10 +945,28 @@ export const createLangGraphNodeHandlers = (deps: LangGraphNodeDependencies) =>
error: safety.reason
});
await runtime.emitStep(latestStep(trace));
if (shouldFallbackToGeneration) {
return {
...trace,
error: safety.reason,
safetyDecision: safety,
savedPriorSqlResolution: {
...state.savedPriorSqlResolution,
safetyRejected: true,
fallbackToGeneration: true,
reused: true
},
savedPriorSqlFallbackRequested: true,
sql: undefined,
explanation: undefined,
terminalStatus: undefined
};
}
return {
...trace,
error: safety.reason,
safetyDecision: safety,
savedPriorSqlFallbackRequested: false,
terminalStatus: "rejected"
};
}
Expand Down Expand Up @@ -973,7 +997,82 @@ export const createLangGraphNodeHandlers = (deps: LangGraphNodeDependencies) =>
await runtime.emitStep(latestStep(trace));
return {
...trace,
safetyDecision: safety
safetyDecision: safety,
savedPriorSqlFallbackRequested: false
};
};

const resolveSavedPriorSql = async (
state: LangGraphState,
config?: LangGraphRunnableConfig
) => {
const startedAt = new Date().toISOString();
const runtime = createNodeRuntime(config);
await runtime.emitStep(
buildRunningStep(
state,
"resolve-saved-prior-sql",
startedAt,
"正在评估是否复用已保存 SQL"
)
);
const resolution = deps.resolveSavedPriorSqlNode.run({
retrievalBundle: state.retrievalBundle,
question: state.question
});
const endedAt = new Date().toISOString();
const detail =
resolution.status === "hit"
? "命中已保存 SQL,跳过 LLM SQL 生成。"
: `未命中 saved prior shortcut,状态=${resolution.status}。`;
const trace = appendStep(state, {
step: {
node: "resolve-saved-prior-sql",
status: resolution.status === "hit" ? "success" : "skipped",
detail,
...withTiming(startedAt, endedAt),
outputSummary: summarize({
status: resolution.status,
reasonCodes: resolution.reasonCodes,
selectedChunkId: resolution.selectedChunkId,
selectedViewId: resolution.selectedViewId,
selectedSourceRunId: resolution.selectedSourceRunId
})
},
outputs: {
status: resolution.status,
reasonCodes: resolution.reasonCodes,
selectedChunkId: resolution.selectedChunkId,
selectedViewId: resolution.selectedViewId,
selectedSourceRunId: resolution.selectedSourceRunId
}
});
await runtime.emitStep(latestStep(trace));
if (resolution.status !== "hit" || !resolution.sql) {
return {
...trace,
savedPriorSqlResolution: {
...resolution,
reused: false,
safetyRejected: false,
fallbackToGeneration: false
},
savedPriorSqlFallbackRequested: false
};
}
return {
...trace,
model: "saved-prior-sql",
sql: resolution.sql,
explanation: resolution.explanation,
error: undefined,
savedPriorSqlResolution: {
...resolution,
reused: true,
safetyRejected: false,
fallbackToGeneration: false
},
savedPriorSqlFallbackRequested: false
};
};

Expand Down Expand Up @@ -1144,6 +1243,7 @@ export const createLangGraphNodeHandlers = (deps: LangGraphNodeDependencies) =>
buildIntentPlan,
buildSemanticQuery,
buildPhysicalPlan,
resolveSavedPriorSql,
generateSql,
safetyCheck,
executeSql,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ClarifyNode } from "../nodes/clarify.node";
import { ExecuteSqlNode } from "../nodes/execute-sql.node";
import { FormatAnswerNode } from "../nodes/format-answer.node";
import { GenerateSqlNode } from "../nodes/generate-sql.node";
import { ResolveSavedPriorSqlNode } from "../nodes/resolve-saved-prior-sql.node";
import { RetrieveKnowledgeNode } from "../nodes/retrieve-knowledge.node";
import { SafetyCheckNode } from "../nodes/safety-check.node";
import {
Expand Down Expand Up @@ -46,6 +47,8 @@ const LangGraphStateAnnotation = Annotation.Root({
physicalPlan: Annotation<LangGraphState["physicalPlan"]>(),
planningStatus: Annotation<LangGraphState["planningStatus"]>(),
planningWarnings: Annotation<string[] | undefined>(),
savedPriorSqlResolution: Annotation<LangGraphState["savedPriorSqlResolution"]>(),
savedPriorSqlFallbackRequested: Annotation<boolean | undefined>(),
safetyDecision: Annotation<LangGraphState["safetyDecision"]>(),
sql: Annotation<string | undefined>(),
explanation: Annotation<string | undefined>(),
Expand All @@ -69,6 +72,7 @@ export const createLangGraphRuntime = (deps: LangGraphNodeDependencies) => {
.addNode("build-intent-plan", handlers.buildIntentPlan)
.addNode("build-semantic-query", handlers.buildSemanticQuery)
.addNode("build-physical-plan", handlers.buildPhysicalPlan)
.addNode("resolve-saved-prior-sql", handlers.resolveSavedPriorSql)
.addNode("generate-sql", handlers.generateSql)
.addNode("safety-check", handlers.safetyCheck)
.addNode("execute-sql", handlers.executeSql)
Expand All @@ -86,7 +90,16 @@ export const createLangGraphRuntime = (deps: LangGraphNodeDependencies) => {
.addEdge("retrieve-knowledge", "build-intent-plan")
.addEdge("build-intent-plan", "build-semantic-query")
.addEdge("build-semantic-query", "build-physical-plan")
.addEdge("build-physical-plan", "generate-sql")
.addEdge("build-physical-plan", "resolve-saved-prior-sql")
.addConditionalEdges(
"resolve-saved-prior-sql",
(state) =>
state.savedPriorSqlResolution?.status === "hit" ? "shortcut" : "continue",
{
shortcut: "safety-check",
continue: "generate-sql"
}
)
.addConditionalEdges(
"generate-sql",
(state) => (state.fatalError ? "fatal" : "continue"),
Expand All @@ -97,8 +110,14 @@ export const createLangGraphRuntime = (deps: LangGraphNodeDependencies) => {
)
.addConditionalEdges(
"safety-check",
(state) => (state.terminalStatus === "rejected" ? "rejected" : "continue"),
(state) => {
if (state.savedPriorSqlFallbackRequested) {
return "fallback-generate";
}
return state.terminalStatus === "rejected" ? "rejected" : "continue";
},
{
"fallback-generate": "generate-sql",
rejected: END,
continue: "execute-sql"
}
Expand Down Expand Up @@ -142,6 +161,7 @@ export class LangGraphRuntimeService {
private readonly buildIntentPlanNode: BuildIntentPlanNode,
private readonly buildSemanticQueryNode: BuildSemanticQueryNode,
private readonly buildPhysicalPlanNode: BuildPhysicalPlanNode,
private readonly resolveSavedPriorSqlNode: ResolveSavedPriorSqlNode,
private readonly generateSqlNode: GenerateSqlNode,
private readonly safetyNode: SafetyCheckNode,
private readonly executeNode: ExecuteSqlNode,
Expand All @@ -153,6 +173,7 @@ export class LangGraphRuntimeService {
buildIntentPlanNode: this.buildIntentPlanNode,
buildSemanticQueryNode: this.buildSemanticQueryNode,
buildPhysicalPlanNode: this.buildPhysicalPlanNode,
resolveSavedPriorSqlNode: this.resolveSavedPriorSqlNode,
generateSqlNode: this.generateSqlNode,
safetyNode: this.safetyNode,
executeNode: this.executeNode,
Expand Down
Loading
Loading