File size: 5,344 Bytes
e5faeaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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 }
  },
}))