agent / src /store /agentStore.js
shutajb's picture
Upload 22 files
e5faeaf verified
Raw History Blame Contribute Delete
5.34 kB
import { create } from 'zustand'
import { toOpenAITools } from '../utils/tools'
import { safeCalc } from '../utils/calc'
const STEP_LIMIT = 6
const TIMEOUT_MS = 120000
const DEFAULT_MODEL = 'nvidia/nemotron-3-ultra-550b-a55b:free'
/* ------------------------------------------------------------------ *
* 工具執行
* ------------------------------------------------------------------ */
function executeTool(call) {
const name = call?.function?.name
let args
try {
args = JSON.parse(call?.function?.arguments || '{}')
} catch {
return '工具參數不是合法 JSON'
}
switch (name) {
case 'search_web':
return `【search_web】已查詢「${args.query}」。` +
'(純前端環境沒有網路存取權限,此為模擬回應;要真的搜尋需改接後端服務)'
case 'calculate':
return safeCalc(args.expression)
default:
return `【${name || '未知工具'}】此環境未實作,收到參數 ${JSON.stringify(args)}`
}
}
/* ------------------------------------------------------------------ *
* 前置檢查與單一請求
* ------------------------------------------------------------------ */
function validateConfig(apiConfig) {
if (!apiConfig) return '尚未設定 API 連線'
if (!apiConfig.apiKey?.trim()) return '尚未設定 API Key(請先開「設定」填入)'
if (!apiConfig.supplier?.trim()) return '尚未設定 API URL'
if (!apiConfig.model?.trim()) return '尚未設定 Model 名稱'
return ''
}
async function requestCompletion(messages, toolsList, apiConfig) {
const base = String(apiConfig.supplier).replace(/\/+$/, '')
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
try {
const res = await fetch(`${base}/chat/completions`, {
method: 'POST',
signal: controller.signal,
headers: {
Authorization: `Bearer ${apiConfig.apiKey.trim()}`,
'Content-Type': 'application/json',
'HTTP-Referer': typeof window !== 'undefined' ? window.location.origin : 'https://agent-studio',
'X-Title': 'Agent Studio',
},
body: JSON.stringify({
model: apiConfig.model,
messages,
tools: toOpenAITools(toolsList),
stream: false,
}),
})
let data = null
try { data = await res.json() } catch { /* 非 JSON 回應 */ }
if (!res.ok) {
const detail = data?.error?.message
|| data?.error?.data?.message
|| data?.message
|| (typeof data === 'string' ? data : '無錯誤訊息')
return { ok: false, error: `API 錯誤 ${res.status}:${detail}` }
}
return { ok: true, data }
} catch (e) {
if (e.name === 'AbortError') {
return { ok: false, error: `請求逾時(超過 ${TIMEOUT_MS / 1000} 秒未回應)` }
}
return { ok: false, error: `網路錯誤:${e.message}` }
} finally {
clearTimeout(timer)
}
}
/* ------------------------------------------------------------------ *
* Store
* ------------------------------------------------------------------ */
export const useAgentStore = create((set, get) => ({
messages: [],
running: false,
step: 0,
error: '',
reset() {
if (get().running) return
set({ messages: [], step: 0, error: '' })
},
async runAgent(input, toolsList, apiConfig, agent) {
const preflight = validateConfig(apiConfig)
if (preflight) {
set({ error: preflight })
return { ok: false, error: preflight }
}
if (get().running) {
set({ error: '已有任務正在執行,請稍候或清空對話' })
return { ok: false, error: 'running' }
}
// system prompt 每輪重設,歷史訊息保留
const history = get().messages.filter((m) => m.role !== 'system')
const seed = agent?.systemPrompt
? [{ role: 'system', content: agent.systemPrompt }, ...history]
: history
set({ running: true, error: '', step: 0 })
const userMsg = { role: 'user', content: input }
set((s) => ({ messages: [...s.messages, userMsg] }))
const messages = [...seed, userMsg]
const add = (msg) => set((s) => ({ messages: [...s.messages, msg], error: '' }))
for (let step = 1; step <= STEP_LIMIT; step++) {
set({ step })
const result = await requestCompletion(messages, toolsList, apiConfig)
if (!result.ok) {
add({ role: 'assistant', content: `⚠️ ${result.error}` })
break
}
const message = result.data?.choices?.[0]?.message
if (!message) {
add({ role: 'assistant', content: '⚠️ API 回應格式異常(choices 為空)' })
break
}
const assistantMsg = {
role: 'assistant',
content: message.content || '',
tool_calls: message.tool_calls || [],
}
messages.push(assistantMsg)
add(assistantMsg)
const calls = assistantMsg.tool_calls
if (calls.length === 0) break
for (const call of calls) {
const toolMsg = {
role: 'tool',
tool_call_id: call.id,
name: call.function?.name,
arguments: call.function?.arguments,
content: executeTool(call),
}
messages.push(toolMsg)
add(toolMsg)
}
}
set({ running: false, step: 0 })
return { ok: true }
},
}))