Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/app/service/agent/service_worker/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { AgentOPFSService } from "./opfs_service";
import { executeSkillScript } from "@App/app/service/offscreen/client";
import { createTabTools } from "@App/app/service/agent/core/tools/tab_tools";
import { ChatService } from "./chat_service";
import { PromptOptimizerService } from "./prompt_optimizer_service";

// 保留对外 API(测试文件直接从 "./agent" import 这三个函数)
export { isRetryableError, withRetry, classifyErrorCode } from "./retry_utils";
Expand Down Expand Up @@ -78,6 +79,7 @@ export class AgentService {
private toolLoopOrchestrator!: ToolLoopOrchestrator;
// 主聊天入口及会话 CRUD 委托给 ChatService
private chatService!: ChatService;
private promptOptimizerService: PromptOptimizerService;

constructor(
private group: Group,
Expand All @@ -88,6 +90,7 @@ export class AgentService {
this.modelService = new AgentModelService(group);
this.opfsService = new AgentOPFSService(sender);
this.llmClient = new LLMClient(agentChatRepo);
this.promptOptimizerService = new PromptOptimizerService(this.modelService, this.llmClient);
this.compactService = new CompactService(
this.modelService,
{
Expand Down Expand Up @@ -155,6 +158,12 @@ export class AgentService {
this.group.on("attachToConversation", this.handleAttachToConversation.bind(this));
// 获取正在运行的会话 ID 列表
this.group.on("getRunningConversationIds", () => this.getRunningConversationIds());
this.group.on("optimizePrompt", (params: { requestId: string; prompt: string; modelId?: string }) =>
this.promptOptimizerService.optimizePrompt(params)
);
this.group.on("cancelPromptOptimization", (requestId: string) =>
this.promptOptimizerService.cancelOptimization(requestId)
);
// Skill 管理(供 Options UI 调用)
this.group.on(
"installSkill",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it, vi } from "vitest";
import type { AgentModelConfig } from "../core/types";
import { PROMPT_OPTIMIZER_SYSTEM_PROMPT, PromptOptimizerService } from "./prompt_optimizer_service";

describe("提示词优化服务", () => {
it("应使用所选模型并要求保留原意和输入语言", async () => {
const model: AgentModelConfig = {
id: "model-1",
name: "Model 1",
provider: "openai",
apiBaseUrl: "https://example.com",
apiKey: "test-key",
model: "model-1",
};
const getModel = vi.fn().mockResolvedValue(model);
const callLLM = vi.fn().mockResolvedValue({ content: " 优化后的提示词 " });
const service = new PromptOptimizerService({ getModel }, { callLLM });

await expect(
service.optimizePrompt({ requestId: "request-1", prompt: " 帮我分析数据 ", modelId: "model-1" })
).resolves.toBe("优化后的提示词");

expect(getModel).toHaveBeenCalledWith("model-1");
expect(PROMPT_OPTIMIZER_SYSTEM_PROMPT).toContain("same language");
expect(PROMPT_OPTIMIZER_SYSTEM_PROMPT).toContain("Preserve the original intent");
expect(callLLM).toHaveBeenCalledWith(
model,
{
messages: [
{ role: "system", content: PROMPT_OPTIMIZER_SYSTEM_PROMPT },
{ role: "user", content: "帮我分析数据" },
],
cache: false,
},
expect.any(Function),
expect.any(AbortSignal)
);
});

it("空输入或空响应应报错", async () => {
const getModel = vi.fn().mockResolvedValue({ id: "model-1" });
const callLLM = vi.fn().mockResolvedValue({ content: " " });
const service = new PromptOptimizerService({ getModel }, { callLLM });

await expect(service.optimizePrompt({ requestId: "empty", prompt: " ", modelId: "model-1" })).rejects.toThrow(
"Prompt cannot be empty"
);
await expect(
service.optimizePrompt({ requestId: "empty-response", prompt: "draft", modelId: "model-1" })
).rejects.toThrow("empty response");
});

it("取消请求时应中止进行中的模型调用", async () => {
let signal: AbortSignal | undefined;
const callLLM = vi.fn((_model, _params, _sendEvent, callSignal: AbortSignal) => {
signal = callSignal;
return new Promise<never>((_resolve, reject) => {
callSignal.addEventListener("abort", () => reject(callSignal.reason), { once: true });
});
});
const service = new PromptOptimizerService({ getModel: vi.fn().mockResolvedValue({ id: "model-1" }) }, { callLLM });

const optimizing = service.optimizePrompt({ requestId: "request-to-cancel", prompt: "draft" });
await vi.waitFor(() => expect(signal).toBeDefined());
expect(service.cancelOptimization("request-to-cancel")).toBe(true);

await expect(optimizing).rejects.toBeDefined();
expect(signal?.aborted).toBe(true);
});
});
71 changes: 71 additions & 0 deletions src/app/service/agent/service_worker/prompt_optimizer_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { AgentModelConfig, ChatRequest, ChatStreamEvent } from "../core/types";
import type { LLMCallResult } from "./llm_client";

export const PROMPT_OPTIMIZER_SYSTEM_PROMPT =
"You are a prompt engineering expert. Rewrite the user's raw input into a clear, structured, and actionable prompt that an AI agent can execute. Preserve the original intent, requirements, constraints, and factual details. Always respond in the same language as the user's input. Output only the optimized prompt text, with no explanation, preamble, or markdown fence.";

interface PromptOptimizerModelSource {
getModel(modelId?: string): Promise<AgentModelConfig>;
}

interface PromptOptimizerLLM {
callLLM(
model: AgentModelConfig,
params: { messages: ChatRequest["messages"]; cache?: boolean },
sendEvent: (event: ChatStreamEvent) => void,
signal: AbortSignal
): Promise<LLMCallResult>;
}

export class PromptOptimizerService {
private readonly activeRequests = new Map<string, AbortController>();

constructor(
private readonly modelSource: PromptOptimizerModelSource,
private readonly llm: PromptOptimizerLLM
) {}

async optimizePrompt(params: { requestId: string; prompt: string; modelId?: string }): Promise<string> {
const prompt = params.prompt.trim();
if (!prompt) throw new Error("Prompt cannot be empty");

const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(new DOMException("Prompt optimization timed out", "TimeoutError")),
60_000
);
this.activeRequests.set(params.requestId, controller);

try {
const model = await this.modelSource.getModel(params.modelId);
const result = await this.llm.callLLM(
model,
{
messages: [
{ role: "system", content: PROMPT_OPTIMIZER_SYSTEM_PROMPT },
{ role: "user", content: prompt },
],
cache: false,
},
() => {},
controller.signal
);
const optimized = result.content.trim();
if (!optimized) throw new Error("Prompt optimizer returned an empty response");
return optimized;
} finally {
clearTimeout(timeout);
if (this.activeRequests.get(params.requestId) === controller) {
this.activeRequests.delete(params.requestId);
}
}
}

cancelOptimization(requestId: string): boolean {
const controller = this.activeRequests.get(requestId);
if (!controller) return false;
controller.abort(new DOMException("Prompt optimization cancelled", "AbortError"));
this.activeRequests.delete(requestId);
return true;
}
}
8 changes: 8 additions & 0 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,14 @@ export class AgentClient extends Client {
return this.do("setSummaryModelId", id);
}

optimizePrompt(params: { requestId: string; prompt: string; modelId?: string }): Promise<string> {
return this.doThrow("optimizePrompt", params);
}

cancelPromptOptimization(requestId: string): Promise<void> {
return this.do("cancelPromptOptimization", requestId).then(() => undefined);
}

// 搜索配置
getSearchConfig(): Promise<SearchEngineConfig> {
return this.doThrow("getSearchConfig");
Expand Down
4 changes: 4 additions & 0 deletions src/locales/de-DE/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "Abbrechen",
"chat_message_queued": "In Warteschlange",
"chat_cancel_message": "Senden abbrechen",
"chat_prompt_optimize": "Prompt optimieren",
"chat_prompt_optimizing": "Prompt wird optimiert…",
"chat_prompt_optimized": "Prompt optimiert",
"chat_prompt_optimize_failed": "Prompt konnte nicht optimiert werden: {{error}}",
"permission_title": "Das Skript fordert die Verwendung des Agent-Gesprächs an",
"permission_describe": "Dieses Skript fordert Zugang zur Agent-Gesprächsfunktion an, was API-Token verbraucht. Erlauben Sie nur vertrauenswürdigen Skripten.",
"permission_content": "Agent-Gespräch",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en-US/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "Cancel",
"chat_message_queued": "Queued",
"chat_cancel_message": "Cancel send",
"chat_prompt_optimize": "Optimize prompt",
"chat_prompt_optimizing": "Optimizing prompt…",
"chat_prompt_optimized": "Prompt optimized",
"chat_prompt_optimize_failed": "Could not optimize prompt: {{error}}",
"permission_title": "The script is requesting to use Agent conversation",
"permission_describe": "This script requests Agent conversation access, which will consume API tokens. Only grant access to trusted scripts.",
"permission_content": "Agent Conversation",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ja-JP/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "キャンセル",
"chat_message_queued": "キュー中",
"chat_cancel_message": "送信をキャンセル",
"chat_prompt_optimize": "プロンプトを最適化",
"chat_prompt_optimizing": "プロンプトを最適化中…",
"chat_prompt_optimized": "プロンプトを最適化しました",
"chat_prompt_optimize_failed": "プロンプトを最適化できませんでした: {{error}}",
"permission_title": "スクリプトが Agent 会話の使用をリクエストしています",
"permission_describe": "このスクリプトは Agent 会話機能の使用をリクエストしており、API トークンを消費します。信頼できるスクリプトにのみ許可してください。",
"permission_content": "Agent 会話",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ko-KR/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "취소",
"chat_message_queued": "대기 중",
"chat_cancel_message": "전송 취소",
"chat_prompt_optimize": "프롬프트 최적화",
"chat_prompt_optimizing": "프롬프트 최적화 중…",
"chat_prompt_optimized": "프롬프트가 최적화되었습니다",
"chat_prompt_optimize_failed": "프롬프트를 최적화하지 못했습니다: {{error}}",
"permission_title": "스크립트가 에이전트 대화 사용을 요청합니다",
"permission_describe": "이 스크립트는 에이전트 대화 기능 사용을 요청하며, 이는 API 토큰을 소비합니다. 신뢰할 수 있는 스크립트에만 허용하세요.",
"permission_content": "에이전트 대화",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/pt-BR/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "Cancelar",
"chat_message_queued": "Na fila",
"chat_cancel_message": "Cancelar envio",
"chat_prompt_optimize": "Otimizar prompt",
"chat_prompt_optimizing": "Otimizando prompt…",
"chat_prompt_optimized": "Prompt otimizado",
"chat_prompt_optimize_failed": "Não foi possível otimizar o prompt: {{error}}",
"permission_title": "O script está solicitando o uso da conversa do Agente",
"permission_describe": "Este script solicita acesso à conversa do Agente, o que consumirá tokens da API. Conceda acesso apenas a scripts confiáveis.",
"permission_content": "Conversa do Agente",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ru-RU/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "Отмена",
"chat_message_queued": "В очереди",
"chat_cancel_message": "Отменить отправку",
"chat_prompt_optimize": "Оптимизировать промпт",
"chat_prompt_optimizing": "Оптимизация промпта…",
"chat_prompt_optimized": "Промпт оптимизирован",
"chat_prompt_optimize_failed": "Не удалось оптимизировать промпт: {{error}}",
"permission_title": "Скрипт запрашивает использование Agent разговора",
"permission_describe": "Этот скрипт запрашивает доступ к функции Agent разговора, что будет потреблять API токены. Разрешайте только доверенным скриптам.",
"permission_content": "Agent разговор",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/tr-TR/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "İptal",
"chat_message_queued": "Sıraya alındı",
"chat_cancel_message": "Göndermeyi iptal et",
"chat_prompt_optimize": "İstemi optimize et",
"chat_prompt_optimizing": "İstem optimize ediliyor…",
"chat_prompt_optimized": "İstem optimize edildi",
"chat_prompt_optimize_failed": "İstem optimize edilemedi: {{error}}",
"permission_title": "Betik, Ajan konuşmasını kullanmak istiyor",
"permission_describe": "Bu betik, API tokenlerini tüketecek olan Ajan konuşma erişimi istiyor. Yalnızca güvenilir betiklere erişim izni verin.",
"permission_content": "Ajan Konuşması",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/vi-VN/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "Hủy",
"chat_message_queued": "Đang chờ",
"chat_cancel_message": "Hủy gửi",
"chat_prompt_optimize": "Tối ưu hóa câu lệnh",
"chat_prompt_optimizing": "Đang tối ưu hóa câu lệnh…",
"chat_prompt_optimized": "Đã tối ưu hóa câu lệnh",
"chat_prompt_optimize_failed": "Không thể tối ưu hóa câu lệnh: {{error}}",
"permission_title": "Script yêu cầu sử dụng cuộc trò chuyện Agent",
"permission_describe": "Script này yêu cầu quyền truy cập chức năng cuộc trò chuyện Agent, sẽ tiêu thụ API token. Chỉ cấp quyền cho các script đáng tin cậy.",
"permission_content": "Cuộc trò chuyện Agent",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-CN/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "取消",
"chat_message_queued": "排队中",
"chat_cancel_message": "取消发送",
"chat_prompt_optimize": "优化提示词",
"chat_prompt_optimizing": "正在优化提示词…",
"chat_prompt_optimized": "提示词已优化",
"chat_prompt_optimize_failed": "无法优化提示词:{{error}}",
"permission_title": "脚本请求使用 Agent 对话",
"permission_describe": "此脚本请求使用 Agent 对话功能,将消耗 API Token。请仅对可信脚本授权。",
"permission_content": "Agent 对话",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/zh-TW/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"chat_cancel_edit": "取消",
"chat_message_queued": "排隊中",
"chat_cancel_message": "取消傳送",
"chat_prompt_optimize": "最佳化提示詞",
"chat_prompt_optimizing": "正在最佳化提示詞…",
"chat_prompt_optimized": "提示詞已最佳化",
"chat_prompt_optimize_failed": "無法最佳化提示詞:{{error}}",
"permission_title": "腳本請求使用 Agent 對話",
"permission_describe": "此腳本請求使用 Agent 對話功能,將消耗 API Token。請僅對可信腳本授權。",
"permission_content": "Agent 對話",
Expand Down
3 changes: 3 additions & 0 deletions src/pages/options/routes/Agent/Chat/ChatArea.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { notify } from "@App/pages/components/ui/toast";
import { agentClient } from "@App/pages/store/features/script";
import { Bot } from "lucide-react";
import type {
AgentModelConfig,
Expand Down Expand Up @@ -766,6 +767,8 @@ export default function ChatArea({
backgroundEnabled={backgroundEnabled}
onBackgroundEnabledChange={onBackgroundEnabledChange}
hasPendingMessage={pendingMessageId !== null}
onOptimizePrompt={(prompt, modelId, requestId) => agentClient.optimizePrompt({ requestId, prompt, modelId })}
onCancelOptimizePrompt={(requestId) => agentClient.cancelPromptOptimization(requestId)}
/>
{noModel && (
<div data-testid="no-model-hint" className="text-center text-xs text-muted-foreground pb-2">
Expand Down
Loading
Loading