From 1cf9232bb7453b97060950fe3a65e46138a45d72 Mon Sep 17 00:00:00 2001 From: Ancss <61501274+Ancss@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:05:24 +0800 Subject: [PATCH] finished the first version --- src-tauri/src/ai/openai.rs | 8 +- src-tauri/src/prompts.rs | 39 ++++--- src-tauri/src/utils.rs | 2 +- src/components/OsaiApp.tsx | 205 ++++++++++++++++++--------------- src/components/SideDrawer .tsx | 4 +- src/config/aiProviders.ts | 50 +++----- src/hooks/useAI.ts | 7 +- src/hooks/useSettings.ts | 5 +- src/type.ts | 3 +- src/utils/i18n.ts | 10 +- 10 files changed, 168 insertions(+), 165 deletions(-) diff --git a/src-tauri/src/ai/openai.rs b/src-tauri/src/ai/openai.rs index b3ff858..6dac2f2 100644 --- a/src-tauri/src/ai/openai.rs +++ b/src-tauri/src/ai/openai.rs @@ -20,13 +20,7 @@ impl AIModel for OpenAI { plan: vec!["Step 1".to_string(), "Step 2".to_string()], user_confirmation_required: false, // confirmation_message: None, - execution: vec![ - PlatformSpecificStep { - windows: r#"powershell -Command "Get-Process | Where-Object {$_.MainWindowTitle -like '*Notepad*'} | Select-Object -First 1 | Stop-Process""#.to_string(), - macos: r#"pkill -x Notes"#.to_string(), - linux: r#"pkill gedit"#.to_string(), - }, - ], + execution: String, response: "This is a sample response".to_string(), }) } diff --git a/src-tauri/src/prompts.rs b/src-tauri/src/prompts.rs index 058f272..77d495b 100644 --- a/src-tauri/src/prompts.rs +++ b/src-tauri/src/prompts.rs @@ -1,27 +1,30 @@ use sysinfo::{System, SystemExt}; pub const SYSTEM_PROMPT_TEMPLATE: &str = r#" -You are an intelligent operating system assistant (OS AI) capable of performing tasks on the user's behalf. Your primary function is to understand user requests and execute them using available system resources. +You are an intelligent operating system assistant (OS AI) capable of performing tasks on the user's behalf using PowerShell commands. Your primary function is to understand user requests and prepare to execute them using available system resources. Guidelines: 1. Provide concise and accurate responses for general queries. -2. For actionable requests, take initiative to perform tasks without asking for permission, unless it involves system changes or sensitive operations. -3. Utilize system resources efficiently to fulfill user requests. -4. Break down tasks into the simplest, most atomic steps possible. -5. Generate actual, executable system commands for each step. -6. Only set user_confirmation_required to true for operations that modify system settings or access sensitive data. -7. If a task cannot be completed, explain why and suggest alternatives. -8. Do not invent or assume any information not explicitly provided or directly obtainable through the executed commands. -9. For web searches, use general search engines like Google or Bing unless a specific, verified website is needed. -10. When dealing with applications: - a. Do not assume default installation paths. - b. Use system commands to search for the application in multiple potential locations. - c. Verify the existence of the application before attempting to launch it. - d. Provide clear feedback on whether the application was found and launched successfully. -11. Combine interdependent steps into a single executable step when necessary. -12. The 'code' field in the execution array must contain only executable commands. Do not include comments or pseudocode. -13. Be aware of and utilize appropriate system commands for different operating systems (Windows, macOS, Linux). -14. Always provide a clear and informative response to the user about the actions taken and their results. +2. For actionable requests, generate a single line of PowerShell code to perform all required actions. +3. The execution field must contain only PowerShell code, with all necessary actions combined into a single line. +4. Do not assume default installation paths for applications. Use PowerShell commands to search for applications when necessary. +5. Verify the existence of applications before attempting to launch them. +6. Set user_confirmation_required to true for operations that modify system settings or access sensitive data. This includes, but is not limited to: + - Creating, modifying, or deleting environment variables + - Changing system configurations + - Installing or uninstalling software + - Modifying user accounts or permissions + - Accessing or modifying sensitive user data +7. When user_confirmation_required is true: + - The response should clearly state that the action requires user confirmation before execution. + - Use language that indicates the action is prepared but not yet executed, such as "I'm ready to..." or "I've prepared the command to...". + - Ask the user if they want to proceed with the action. + - Do not imply or state that the action has already been completed. +8. If a task cannot be completed with PowerShell, explain why and suggest alternatives in the response. +9. Do not invent or assume any information not explicitly provided or directly obtainable through the executed command. +10. For web searches, use general search engines like Google or Bing unless a specific, verified website is needed. +11. Always provide a clear and informative response to the user about the actions prepared and their potential results. +12. If the user request is not a task (e.g., general question), leave the execution field as an empty string. Always structure your response using the specified AIResponse format. diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 717637d..c93b995 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -6,7 +6,7 @@ pub struct AIResponse { pub plan: Vec, pub user_confirmation_required: bool, // pub confirmation_message: Option, - pub execution: Vec, + pub execution: String, pub response: String, } diff --git a/src/components/OsaiApp.tsx b/src/components/OsaiApp.tsx index 74a05b8..239d0e0 100644 --- a/src/components/OsaiApp.tsx +++ b/src/components/OsaiApp.tsx @@ -31,9 +31,6 @@ const OsaiApp = ({ const { sendMessage, isLoading, abortRequest, executeCode } = useAI(); const messageEndRef = useRef(null); - useEffect(() => { - messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); useEffect(() => { latestMessagesRef.current = messages; messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -46,45 +43,49 @@ const OsaiApp = ({ role: "user", content: [{ type: "text", text: input }], }; - setMessages((prev) => [ - ...prev, - newMessage, - { - role: "assistant", - content: [{ type: "text", text: "..." }], - status: "loading", - }, - ]); + setMessages((prev) => [...prev, newMessage]); setInput(""); setError(""); + try { + // 添加一个临时的 loading 消息 + setMessages((prev) => [ + ...prev, + { + role: "assistant", + content: [{ type: "text", text: "..." }], + status: "loading", + }, + ]); + const aiResponse: AIResponse = await sendMessage([ ...messages, newMessage, ]); + + let executionResult = null; + if (!aiResponse.user_confirmation_required && aiResponse.execution) { + executionResult = await executeCode(aiResponse.execution); + } + const newAssistantMessage: ChatMessage = { role: "assistant", content: [{ type: "text", text: aiResponse.response }], aiResponse, - status: "complete", executionStatus: aiResponse.user_confirmation_required ? "pending" - : "executing", + : "complete", + executionResult, }; - setMessages((prev) => { - const updatedMessages = [...prev.slice(0, -1), newAssistantMessage]; - latestMessagesRef.current = updatedMessages; - if (!aiResponse.user_confirmation_required) { - executeAIResponse(updatedMessages.length - 1); - } - return updatedMessages; - }); + + // 更新消息,替换 loading 消息 + setMessages((prev) => [...prev.slice(0, -1), newAssistantMessage]); } catch (error: any) { - setMessages((prev) => prev.slice(0, -2)); - setInput(newMessage.content[0].text); + setMessages((prev) => prev.slice(0, -2)); // 移除 loading 消息 + setInput(input); setError( error.message.includes("Open settings and set the API key") - ? t("lackApiKey")! + ? t("missingApiKey")! : t("aiResponseError")! ); console.error("Error getting AI response:", error); @@ -93,8 +94,11 @@ const OsaiApp = ({ }; const executeAIResponse = async (messageIndex: number) => { - const message = messages[messageIndex]; - if (message.aiResponse && message.aiResponse.should_execute_code) { + console.log("Executing AI response"); + const currentMessages = latestMessagesRef.current; + const message = currentMessages[messageIndex]; + console.log("Message to execute:", message); + if (message?.aiResponse?.execution) { setMessages((prev) => { const newMessages = [...prev]; newMessages[messageIndex] = { @@ -104,30 +108,14 @@ const OsaiApp = ({ return newMessages; }); - for (const step of message.aiResponse.execution) { - if (!step.code) continue; - const result = await executeCode(step.code); - setMessages((prev) => { - const newMessages = [...prev]; - const executionSteps = - newMessages[messageIndex].aiResponse!.execution; - const stepIndex = executionSteps.findIndex( - (s) => s.step === step.step - ); - executionSteps[stepIndex] = { - ...step, - status: result.success ? "success" : "failure", - result: result.output, - }; - return newMessages; - }); - } + const result = await executeCode(message.aiResponse.execution); setMessages((prev) => { const newMessages = [...prev]; newMessages[messageIndex] = { ...message, executionStatus: "complete", + executionResult: result, }; return newMessages; }); @@ -138,26 +126,48 @@ const OsaiApp = ({ messageIndex: number, confirmed: boolean ) => { - setMessages((prev) => { - const newMessages = [...prev]; - newMessages[messageIndex] = { - ...newMessages[messageIndex], - executionStatus: confirmed ? "executing" : "rejected", - }; - return newMessages; - }); - if (confirmed) { - await executeAIResponse(messageIndex); + const message = messages[messageIndex]; + if (message.aiResponse?.execution) { + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[messageIndex] = { + ...message, + executionStatus: "executing", + }; + return newMessages; + }); + + const result = await executeCode(message.aiResponse.execution); + + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[messageIndex] = { + ...message, + executionStatus: "complete", + executionResult: result, + }; + return newMessages; + }); + } + } else { + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[messageIndex] = { + ...newMessages[messageIndex], + executionStatus: "rejected", + }; + return [...newMessages]; + }); } }; return ( <> -
+
-
+

Osai

- {error && ( - - - {error} - - )} {messages.map((msg, index) => (
-
- {msg.status === "loading" ? "..." : msg.content[0].text} -
-
- {msg.aiResponse?.user_confirmation_required && - msg.executionStatus === "pending" && ( -
- - +
+
+ {msg.status === "loading" ? "..." : msg.content[0].text}
- )} - {/* {msg.aiResponse?.execution && ( -
- {msg.aiResponse.execution - .filter((step) => !!step.code) - .map((step, stepIndex) => ( - - ))} + {msg.aiResponse?.user_confirmation_required && + msg.executionStatus === "pending" && ( +
+ + +
+ )} + {msg.executionResult && ( +
+ {t("ExecutionResult")}: + {msg.executionResult.success + ? t("success") + : t("failure")} +
+ )}
- )} */} +
))} + {error && ( + + + {error} + + )}
diff --git a/src/components/SideDrawer .tsx b/src/components/SideDrawer .tsx index 7c2dd0f..59dc54d 100644 --- a/src/components/SideDrawer .tsx +++ b/src/components/SideDrawer .tsx @@ -12,9 +12,9 @@ const SideDrawer = () => { const screenWidth = window.screen.width; const screenHeight = window.screen.height; if (isExpanded) { - await appWindow.setSize(new PhysicalSize(400, 528)); + await appWindow.setSize(new PhysicalSize(400, 660)); await appWindow.setPosition( - new PhysicalPosition(screenWidth - 380, screenHeight * 0.4) + new PhysicalPosition(screenWidth - 385, screenHeight * 0.3) ); } else { await appWindow.setSize(new PhysicalSize(40, 24)); diff --git a/src/config/aiProviders.ts b/src/config/aiProviders.ts index f073ba5..6da42c2 100644 --- a/src/config/aiProviders.ts +++ b/src/config/aiProviders.ts @@ -26,11 +26,7 @@ export interface AIResponse { user_confirmation_required: boolean; should_execute_code: boolean; // confirmation_message: string; - execution: Array<{ - step: string; - code: string; - result: string; - }>; + execution: string; response: string; } @@ -38,7 +34,7 @@ const tools: Anthropic.Messages.Tool[] = [ { name: "os_ai_assistant", description: - "An AI assistant for executing operating system tasks and answering queries", + "An AI assistant for executing PowerShell tasks and answering queries", input_schema: { type: "object", properties: { @@ -52,7 +48,7 @@ const tools: Anthropic.Messages.Tool[] = [ type: "string", }, description: - "A list of minimal, atomic steps planned to complete the task", + "A list of steps planned to complete the task, even if execution is in one line", }, user_confirmation_required: { type: "boolean", @@ -62,37 +58,17 @@ const tools: Anthropic.Messages.Tool[] = [ should_execute_code: { type: "boolean", description: - "True if the task requires executing system commands or browser actions", + "True if the task requires executing PowerShell commands", }, execution: { - type: "array", - items: { - type: "object", - properties: { - step: { - type: "string", - description: "Description of the execution step", - }, - code: { - type: "string", - description: - "The actual system command to be executed, or an empty string if not applicable", - }, - result: { - type: "string", - description: - "The expected or actual result of the execution step", - }, - }, - required: ["step", "code", "result"], - }, + type: "string", description: - "Detailed execution steps including only steps that require code execution", + "A single line of PowerShell code that executes all required actions for the task, or an empty string if not applicable", }, response: { type: "string", description: - "The final response to the user, including results or next steps, without inventing information", + "The final response to the user, including potential results or next steps, without inventing information", }, }, required: [ @@ -152,10 +128,10 @@ export const aiProviders: AIProvider[] = [ models: ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229"], apiKeyLink: "https://console.anthropic.com/settings/keys", }, - { - name: "GPT", - sendMessage: sendMessageToAnthropic, - models: ["gpt-4", "gpt-3.5-turbo"], - apiKeyLink: "https://platform.openai.com/account/api-keys", - }, + // { + // name: "GPT", + // sendMessage: sendMessageToAnthropic, + // models: ["gpt-4", "gpt-3.5-turbo"], + // apiKeyLink: "https://platform.openai.com/account/api-keys", + // }, ]; diff --git a/src/hooks/useAI.ts b/src/hooks/useAI.ts index e5270f0..43f587f 100644 --- a/src/hooks/useAI.ts +++ b/src/hooks/useAI.ts @@ -9,7 +9,7 @@ import { Message } from "@/type"; export const useAI = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); - const { settings } = useSettings(); + const { settings, getSetting } = useSettings(); const cancelFlagRef = useRef(null); const sendMessage = useCallback( @@ -23,7 +23,9 @@ export const useAI = () => { if (!provider) { throw new Error(`AI provider ${settings.AI_PROVIDER} not found`); } - const apiKey = settings[`${settings.AI_PROVIDER}_API_KEY`]; + const apiKey = getSetting(`${settings.AI_PROVIDER}_API_KEY`); + console.log("API key:", apiKey); + console.log("Settings", settings); if (!apiKey) { throw new Error( `API key for ${settings.AI_PROVIDER} not set, Open settings and set the API key` @@ -70,6 +72,7 @@ export const useAI = () => { const executeCode = useCallback( async (code: string): Promise<{ success: boolean; output: string }> => { try { + console.log("Executing code:", code); const result: string = await invoke("execute_code", { code }); return { success: true, output: result }; } catch (error: any) { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 7e150de..dbd666f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -20,8 +20,9 @@ export const useSettings = () => { }; const getSetting = (key: string): string => { - return settings[key] || ""; + const storedSettings = localStorage.getItem("app_settings"); + const parsedSettings = storedSettings ? JSON.parse(storedSettings) : {}; + return parsedSettings[key] || ""; }; - return { settings, setSetting, getSetting }; }; diff --git a/src/type.ts b/src/type.ts index 26feb89..d88d109 100644 --- a/src/type.ts +++ b/src/type.ts @@ -13,7 +13,7 @@ export interface AIResponse { plan: string[]; user_confirmation_required: boolean; should_execute_code: boolean; - execution: ExecutionStep[]; + execution: string; response: string; } @@ -28,4 +28,5 @@ export interface ChatMessage extends Message { status?: "loading" | "complete"; aiResponse?: AIResponse; executionStatus?: "pending" | "executing" | "complete" | "rejected"; + executionResult?: any; } diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index bb61370..8ad8d91 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -14,7 +14,7 @@ const resources = { aiResponseError: "Failed to get AI response", networkError: "Network error occurred", configurationError: "Configuration error", - lackApiKey: "open settings and provide an API key", + missingApiKey: "open settings and provide an API key", unknownError: "An unknown error occurred", fileNotFound: "File not found", permissionDenied: "Permission denied", @@ -31,6 +31,9 @@ const resources = { requestAborted: "Request was aborted", approve: "Approve", reject: "Reject", + ExecutionResult: "Execution Result", + success: "Success", + failure: "Failure", }, }, zh: { @@ -45,7 +48,7 @@ const resources = { aiResponseError: "获取 AI 响应失败", networkError: "发生网络错误", configurationError: "配置错误", - lackApiKey: "打开设置并提供 API 密钥", + missingApiKey: "打开设置并提供 API 密钥", unknownError: "发生未知错误", fileNotFound: "文件未找到", permissionDenied: "权限被拒绝", @@ -61,6 +64,9 @@ const resources = { requestAborted: "请求被中止", approve: "批准", reject: "拒绝", + ExecutionResult: "执行结果", + success: "成功", + failure: "失败", }, }, };