feat: 实现 Agent 与 DeepSeek 对话链路
This commit is contained in:
parent
2f7d5b2e39
commit
5ca70262e3
@ -12,6 +12,9 @@ pnpm dev
|
||||
|
||||
启动时自动加载项目根目录的 `.env`。
|
||||
|
||||
其中设置 `DEEPSEEK_API_KEY`;
|
||||
也可用 `LLM_TO_AGENT_HOME` 修改数据根目录,用 `DEEPSEEK_MODEL` 修改模型。
|
||||
|
||||
文档:
|
||||
|
||||
- [架构总览](docs/architecture.md)
|
||||
|
||||
95
src/extensions/shared/agent/index.ts
Normal file
95
src/extensions/shared/agent/index.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionRuntimeContext,
|
||||
} from "../../../kernel";
|
||||
import { Event, ExtensionId, Hook } from "../../catalog";
|
||||
import {
|
||||
type ChatMessage,
|
||||
type WorkspaceService,
|
||||
} from "../workspace";
|
||||
|
||||
export interface ModelProvider {
|
||||
id: string;
|
||||
chat(messages: ChatMessage[], signal?: AbortSignal): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
export interface AgentService {
|
||||
chat(input: string, signal?: AbortSignal): AsyncIterable<string>;
|
||||
}
|
||||
|
||||
export function createAgentExtension(): Extension {
|
||||
let runtime: ExtensionRuntimeContext | undefined;
|
||||
let workspace: WorkspaceService | undefined;
|
||||
let model: ModelProvider | undefined;
|
||||
|
||||
const agent: AgentService = {
|
||||
async *chat(input, signal) {
|
||||
if (!runtime || !workspace || !model) {
|
||||
throw new Error("Agent is not running.");
|
||||
}
|
||||
|
||||
const activeRuntime = runtime;
|
||||
const activeWorkspace = workspace;
|
||||
const activeModel = model;
|
||||
const runId = randomUUID();
|
||||
|
||||
const userMessage = await activeWorkspace.append("user", input);
|
||||
await activeRuntime.emit(Event.MessageAdded, {
|
||||
workspaceId: activeWorkspace.workspaceId,
|
||||
conversationId: activeWorkspace.conversationId,
|
||||
message: userMessage,
|
||||
});
|
||||
await activeRuntime.emit(Event.RunStarted, { runId, input });
|
||||
|
||||
let answer = "";
|
||||
|
||||
try {
|
||||
const messages = await activeWorkspace.messages();
|
||||
|
||||
for await (const chunk of activeModel.chat(messages, signal)) {
|
||||
answer += chunk;
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
const assistantMessage = await activeWorkspace.append("assistant", answer);
|
||||
await activeRuntime.emit(Event.MessageAdded, {
|
||||
workspaceId: activeWorkspace.workspaceId,
|
||||
conversationId: activeWorkspace.conversationId,
|
||||
message: assistantMessage,
|
||||
});
|
||||
await activeRuntime.emit(Event.RunCompleted, { runId });
|
||||
} catch (error) {
|
||||
await activeRuntime.emit(Event.RunFailed, { runId, error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
id: ExtensionId.Agent,
|
||||
|
||||
setup(context) {
|
||||
context.add(Hook.Agent, agent);
|
||||
},
|
||||
|
||||
start(context) {
|
||||
const providers = context.all<ModelProvider>(Hook.ModelProviders);
|
||||
|
||||
if (providers.length === 0) {
|
||||
throw new Error("Agent needs at least one model provider.");
|
||||
}
|
||||
|
||||
runtime = context;
|
||||
workspace = context.get<WorkspaceService>(Hook.Workspace);
|
||||
model = providers[0];
|
||||
},
|
||||
|
||||
stop() {
|
||||
runtime = undefined;
|
||||
workspace = undefined;
|
||||
model = undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
96
src/extensions/shared/deepseek/index.ts
Normal file
96
src/extensions/shared/deepseek/index.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import type { Extension } from "../../../kernel";
|
||||
import { ExtensionId, Hook } from "../../catalog";
|
||||
import type { ModelProvider } from "../agent";
|
||||
|
||||
export interface DeepSeekOptions {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
request?: (url: string, init: RequestInit) => Promise<Response>;
|
||||
}
|
||||
|
||||
export function createDeepSeekExtension(options: DeepSeekOptions = {}): Extension {
|
||||
const apiKey = options.apiKey ?? process.env.DEEPSEEK_API_KEY;
|
||||
const baseUrl = (
|
||||
options.baseUrl ??
|
||||
process.env.DEEPSEEK_BASE_URL ??
|
||||
"https://api.deepseek.com"
|
||||
).replace(/\/+$/, "");
|
||||
const model = options.model ?? process.env.DEEPSEEK_MODEL ?? "deepseek-v4-flash";
|
||||
const request = options.request ?? fetch;
|
||||
|
||||
const provider: ModelProvider = {
|
||||
id: "deepseek",
|
||||
|
||||
async *chat(messages, signal) {
|
||||
const response = await request(`${baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: messages.map(({ role, content }) => ({ role, content })),
|
||||
thinking: { type: "disabled" },
|
||||
stream: true,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`DeepSeek request failed (${response.status}): ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("DeepSeek returned an empty response body.");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
|
||||
const events = buffer.split(/\r?\n\r?\n/);
|
||||
buffer = events.pop() ?? "";
|
||||
|
||||
for (const entry of events) {
|
||||
for (const line of entry.split(/\r?\n/)) {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
|
||||
const data = line.slice(5).trim();
|
||||
if (data === "[DONE]") return;
|
||||
if (!data) continue;
|
||||
|
||||
const chunk = JSON.parse(data);
|
||||
const content = chunk.choices?.[0]?.delta?.content;
|
||||
if (content) yield content;
|
||||
}
|
||||
}
|
||||
|
||||
if (done) break;
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
id: ExtensionId.DeepSeek,
|
||||
|
||||
setup(context) {
|
||||
if (!apiKey) {
|
||||
throw new Error("DEEPSEEK_API_KEY is required.");
|
||||
}
|
||||
|
||||
context.add(Hook.ModelProviders, provider);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user