feat: switch from Anthropic to Groq API (llama-3.3-70b-versatile)
All checks were successful
Deploy / deploy (push) Successful in 2m47s
All checks were successful
Deploy / deploy (push) Successful in 2m47s
- route.ts: replace @anthropic-ai/sdk with groq-sdk, rewrite chat loop - voice-tool-schemas.ts: convert from Anthropic format to OpenAI/Groq function tools - voice-history.ts: extend HistoryMessage type to include tool role, simplify cache stubs No prompt caching (Groq does not support it), tool calling preserved.
This commit is contained in:
@@ -2,8 +2,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
|
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Groq from 'groq-sdk'
|
||||||
import { ProxyAgent } from 'undici'
|
|
||||||
|
|
||||||
import { voiceBus } from '@/lib/voice-bus'
|
import { voiceBus } from '@/lib/voice-bus'
|
||||||
import { systemPrompt } from '@/lib/voice-prompts'
|
import { systemPrompt } from '@/lib/voice-prompts'
|
||||||
@@ -12,17 +11,15 @@ import { executeTool } from '@/lib/voice-executors'
|
|||||||
import { cleanForSpeech, stripFillers, isResetCommand } from '@/lib/voice-text'
|
import { cleanForSpeech, stripFillers, isResetCommand } from '@/lib/voice-text'
|
||||||
import {
|
import {
|
||||||
loadHistory, saveHistory, resetHistory,
|
loadHistory, saveHistory, resetHistory,
|
||||||
buildMessagesWithCache, stripCacheControl, HistoryMessage,
|
HistoryMessage,
|
||||||
} from '@/lib/voice-history'
|
} from '@/lib/voice-history'
|
||||||
|
|
||||||
const MODEL = process.env.ANTHROPIC_MODEL || 'claude-haiku-4-5'
|
const MODEL = process.env.GROQ_MODEL || 'llama-3.3-70b-versatile'
|
||||||
const MAX_TOKENS = parseInt(process.env.VOICE_MAX_TOKENS || '300', 10)
|
const MAX_TOKENS = parseInt(process.env.VOICE_MAX_TOKENS || '300', 10)
|
||||||
const MAX_TOOL_ROUNDS = 4
|
const MAX_TOOL_ROUNDS = 4
|
||||||
const RATE_LIMIT_PER_MINUTE = parseInt(process.env.VOICE_RATE_LIMIT || '20', 10)
|
const RATE_LIMIT_PER_MINUTE = parseInt(process.env.VOICE_RATE_LIMIT || '20', 10)
|
||||||
|
|
||||||
// In-memory rate-limit per IP / cookie (host один — Docker контейнер).
|
// In-memory rate-limit per IP / cookie (host один — Docker контейнер).
|
||||||
// Защита от случайного бесконечного цикла или утечки PIN: даже если
|
|
||||||
// auth_token утечёт, вызов /api/voice/chat будет ограничен.
|
|
||||||
const rateBuckets = new Map<string, { count: number; resetAt: number }>()
|
const rateBuckets = new Map<string, { count: number; resetAt: number }>()
|
||||||
function rateLimit(key: string): boolean {
|
function rateLimit(key: string): boolean {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -35,7 +32,6 @@ function rateLimit(key: string): boolean {
|
|||||||
b.count++
|
b.count++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Гигиена: чистим старые бакеты периодически (раз в 5 минут максимум).
|
|
||||||
let lastSweep = 0
|
let lastSweep = 0
|
||||||
function sweep() {
|
function sweep() {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -44,16 +40,12 @@ function sweep() {
|
|||||||
for (const [k, v] of rateBuckets) if (v.resetAt <= now) rateBuckets.delete(k)
|
for (const [k, v] of rateBuckets) if (v.resetAt <= now) rateBuckets.delete(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _client: Anthropic | null = null
|
let _client: Groq | null = null
|
||||||
function client(): Anthropic {
|
function client(): Groq {
|
||||||
if (_client) return _client
|
if (_client) return _client
|
||||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
const apiKey = process.env.GROQ_API_KEY
|
||||||
if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set')
|
if (!apiKey) throw new Error('GROQ_API_KEY not set')
|
||||||
const proxy = process.env.ANTHROPIC_PROXY || process.env.HTTPS_PROXY || ''
|
_client = new Groq({ apiKey })
|
||||||
const fetchOptions = proxy
|
|
||||||
? ({ dispatcher: new ProxyAgent(proxy) } as any)
|
|
||||||
: undefined
|
|
||||||
_client = new Anthropic({ apiKey, fetchOptions })
|
|
||||||
return _client
|
return _client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +61,6 @@ function emitVoice(event: string, agent: 'cosmo' | 'lusya', text?: string) {
|
|||||||
type AgentId = 'cosmo' | 'lusya'
|
type AgentId = 'cosmo' | 'lusya'
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
// Rate-limit по auth_token (или x-voice-internal — для loopback'а от tools).
|
|
||||||
// Идентифицируем клиента: cookie auth_token > x-voice-internal > IP > 'anon'.
|
|
||||||
const cookie = req.headers.get('cookie') || ''
|
const cookie = req.headers.get('cookie') || ''
|
||||||
const tokenMatch = cookie.match(/auth_token=([a-f0-9]{32,})/i)
|
const tokenMatch = cookie.match(/auth_token=([a-f0-9]{32,})/i)
|
||||||
const internal = req.headers.get('x-voice-internal') || ''
|
const internal = req.headers.get('x-voice-internal') || ''
|
||||||
@@ -85,13 +75,11 @@ export async function POST(req: Request) {
|
|||||||
if (!body || typeof body.text !== 'string' || !body.text.trim()) {
|
if (!body || typeof body.text !== 'string' || !body.text.trim()) {
|
||||||
return NextResponse.json({ error: 'text required' }, { status: 400 })
|
return NextResponse.json({ error: 'text required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
const userText: string = body.text.trim().slice(0, 4000) // защита от gigantic prompts
|
const userText: string = body.text.trim().slice(0, 4000)
|
||||||
const agent: AgentId = body.agent === 'lusya' ? 'lusya' : 'cosmo'
|
const agent: AgentId = body.agent === 'lusya' ? 'lusya' : 'cosmo'
|
||||||
|
|
||||||
// Echo command в орб
|
|
||||||
emitVoice('command', agent, userText)
|
emitVoice('command', agent, userText)
|
||||||
|
|
||||||
// Reset-команда — стираем историю и отвечаем шаблонно
|
|
||||||
if (isResetCommand(userText)) {
|
if (isResetCommand(userText)) {
|
||||||
await resetHistory(agent)
|
await resetHistory(agent)
|
||||||
const msg = 'Начинаю новую сессию.'
|
const msg = 'Начинаю новую сессию.'
|
||||||
@@ -99,59 +87,52 @@ export async function POST(req: Request) {
|
|||||||
return NextResponse.json({ text: msg, reset: true })
|
return NextResponse.json({ text: msg, reset: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем историю и добавляем новый user-turn
|
// Загружаем историю и строим messages для Groq (OpenAI-compatible format)
|
||||||
const history = await loadHistory(agent)
|
const history = await loadHistory(agent)
|
||||||
history.push({ role: 'user', content: userText })
|
|
||||||
|
|
||||||
const systemBlocks: Anthropic.TextBlockParam[] = [
|
// Системный prompt + история + новый user message
|
||||||
{
|
const apiMessages: any[] = [
|
||||||
type: 'text',
|
{ role: 'system', content: systemPrompt(agent) },
|
||||||
text: systemPrompt(agent),
|
...history,
|
||||||
cache_control: { type: 'ephemeral' },
|
{ role: 'user', content: userText },
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const apiMessages: Anthropic.MessageParam[] = buildMessagesWithCache(history) as any
|
|
||||||
|
|
||||||
let finalText = ''
|
let finalText = ''
|
||||||
const initialUserIdx = history.length - 1
|
const historyStartLen = apiMessages.length // позиция после которой добавляем новые turns
|
||||||
// Защита от tool-cycling: запоминаем последний (name, args) — если LLM
|
|
||||||
// дважды подряд просит одно и то же, прерываем цикл.
|
// Защита от tool-cycling
|
||||||
let lastToolSig = ''
|
let lastToolSig = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const c = client()
|
const c = client()
|
||||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
const resp = await c.messages.create({
|
const resp = await c.chat.completions.create({
|
||||||
model: MODEL,
|
model: MODEL,
|
||||||
max_tokens: MAX_TOKENS,
|
max_tokens: MAX_TOKENS,
|
||||||
system: systemBlocks,
|
|
||||||
messages: apiMessages,
|
messages: apiMessages,
|
||||||
tools: TOOL_SCHEMAS,
|
tools: TOOL_SCHEMAS as any,
|
||||||
|
tool_choice: 'auto',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const choice = resp.choices[0]
|
||||||
|
const msg = choice.message
|
||||||
const usage = resp.usage as any
|
const usage = resp.usage as any
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[voice/chat] ${agent} round ${round + 1} ${Date.now() - t0}ms · ` +
|
`[voice/chat] ${agent} round ${round + 1} ${Date.now() - t0}ms · ` +
|
||||||
`stop=${resp.stop_reason} · in=${usage?.input_tokens} out=${usage?.output_tokens} ` +
|
`stop=${choice.finish_reason} · in=${usage?.prompt_tokens} out=${usage?.completion_tokens}`
|
||||||
`cache_r=${usage?.cache_read_input_tokens || 0} cache_w=${usage?.cache_creation_input_tokens || 0}`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Разбираем content на text + tool_use
|
// Добавляем assistant message в messages
|
||||||
const toolUses: Anthropic.ToolUseBlock[] = []
|
apiMessages.push(msg)
|
||||||
for (const block of resp.content) {
|
|
||||||
if (block.type === 'text') finalText += block.text
|
|
||||||
else if (block.type === 'tool_use') toolUses.push(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем assistant turn в API messages как есть (важно для tool_use_id)
|
if (choice.finish_reason === 'tool_calls' && msg.tool_calls?.length) {
|
||||||
apiMessages.push({ role: 'assistant', content: resp.content as any })
|
const toolCalls = msg.tool_calls
|
||||||
|
|
||||||
if (resp.stop_reason === 'tool_use' && toolUses.length) {
|
// Loop-guard: сигнатура текущего раунда
|
||||||
// Сигнатура текущего раунда — для loop-guard.
|
const sig = toolCalls
|
||||||
const sig = toolUses
|
.map((tc: any) => `${tc.function.name}:${tc.function.arguments}`)
|
||||||
.map((t) => `${t.name}:${JSON.stringify(t.input)}`)
|
|
||||||
.sort()
|
.sort()
|
||||||
.join('|')
|
.join('|')
|
||||||
if (sig === lastToolSig) {
|
if (sig === lastToolSig) {
|
||||||
@@ -161,25 +142,27 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
lastToolSig = sig
|
lastToolSig = sig
|
||||||
|
|
||||||
const toolResults: Anthropic.ToolResultBlockParam[] = []
|
// Выполняем все tool calls и добавляем результаты
|
||||||
for (const tu of toolUses) {
|
for (const tc of toolCalls) {
|
||||||
console.log(`[voice/chat] tool ${tu.name}(${JSON.stringify(tu.input).slice(0, 200)})`)
|
console.log(`[voice/chat] tool ${tc.function.name}(${tc.function.arguments.slice(0, 200)})`)
|
||||||
const result = await executeTool(tu.name, tu.input, agent)
|
let args: any = {}
|
||||||
toolResults.push({
|
try { args = JSON.parse(tc.function.arguments) } catch (_) {}
|
||||||
type: 'tool_result',
|
const result = await executeTool(tc.function.name, args, agent)
|
||||||
tool_use_id: tu.id,
|
apiMessages.push({
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: tc.id,
|
||||||
content: JSON.stringify(result),
|
content: JSON.stringify(result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
apiMessages.push({ role: 'user', content: toolResults })
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// end_turn / max_tokens / stop_sequence — финальный ответ готов
|
// Финальный ответ
|
||||||
|
finalText = msg.content || ''
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[voice/chat] anthropic error:', e?.message || e)
|
console.error('[voice/chat] groq error:', e?.message || e)
|
||||||
const msg = 'Что-то сломалось.'
|
const msg = 'Что-то сломалось.'
|
||||||
emitVoice('error', agent, msg)
|
emitVoice('error', agent, msg)
|
||||||
return NextResponse.json({ error: 'llm_failed', detail: String(e?.message || e), text: msg }, { status: 502 })
|
return NextResponse.json({ error: 'llm_failed', detail: String(e?.message || e), text: msg }, { status: 502 })
|
||||||
@@ -191,15 +174,13 @@ export async function POST(req: Request) {
|
|||||||
return NextResponse.json({ text: msg }, { status: 200 })
|
return NextResponse.json({ text: msg }, { status: 200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем все turn'ы после initial user (включая tool_use / tool_result)
|
// Сохраняем новые turns в историю (без системного prompt'а)
|
||||||
const newTurns = apiMessages.slice(initialUserIdx + 1)
|
const newTurns = apiMessages.slice(historyStartLen)
|
||||||
for (const turn of newTurns) {
|
const updatedHistory: HistoryMessage[] = [
|
||||||
history.push({
|
...history,
|
||||||
role: turn.role as 'user' | 'assistant',
|
...newTurns.map((m: any) => ({ role: m.role, content: m.content ?? null, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id } as HistoryMessage)),
|
||||||
content: stripCacheControl(turn.content),
|
]
|
||||||
} as HistoryMessage)
|
await saveHistory(agent, updatedHistory)
|
||||||
}
|
|
||||||
await saveHistory(agent, history)
|
|
||||||
|
|
||||||
const cleaned = cleanForSpeech(stripFillers(finalText))
|
const cleaned = cleanForSpeech(stripFillers(finalText))
|
||||||
emitVoice('response', agent, cleaned)
|
emitVoice('response', agent, cleaned)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* /data — это volume контейнера (на хосте /opt/digital-home/smart-home-tablet-data/).
|
* /data — это volume контейнера (на хосте /opt/digital-home/smart-home-tablet-data/).
|
||||||
*
|
*
|
||||||
* Fallback: если /data не существует (локальная разработка) — пишем в /tmp/voice-history.
|
* Fallback: если /data не существует (локальная разработка) — пишем в /tmp/voice-history.
|
||||||
|
*
|
||||||
|
* Формат обновлён под Groq/OpenAI: role может быть 'user' | 'assistant' | 'tool'.
|
||||||
*/
|
*/
|
||||||
import { promises as fs } from 'node:fs'
|
import { promises as fs } from 'node:fs'
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
@@ -10,15 +12,16 @@ import path from 'node:path'
|
|||||||
|
|
||||||
const PRIMARY_DIR = process.env.VOICE_HISTORY_DIR || '/data/voice-history'
|
const PRIMARY_DIR = process.env.VOICE_HISTORY_DIR || '/data/voice-history'
|
||||||
const DATA_DIR = (() => {
|
const DATA_DIR = (() => {
|
||||||
// Проверяем существование родителя (/data) — без него запись упадёт ENOENT.
|
|
||||||
const parent = path.dirname(PRIMARY_DIR)
|
const parent = path.dirname(PRIMARY_DIR)
|
||||||
return existsSync(parent) ? PRIMARY_DIR : '/tmp/voice-history'
|
return existsSync(parent) ? PRIMARY_DIR : '/tmp/voice-history'
|
||||||
})()
|
})()
|
||||||
const MAX_HISTORY = parseInt(process.env.VOICE_MAX_HISTORY || '40', 10)
|
const MAX_HISTORY = parseInt(process.env.VOICE_MAX_HISTORY || '40', 10)
|
||||||
|
|
||||||
export type HistoryMessage = {
|
export type HistoryMessage = {
|
||||||
role: 'user' | 'assistant'
|
role: 'user' | 'assistant' | 'tool'
|
||||||
content: any
|
content: any
|
||||||
|
tool_calls?: any[]
|
||||||
|
tool_call_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function todayIso(): string {
|
function todayIso(): string {
|
||||||
@@ -60,7 +63,8 @@ export async function resetHistory(agent: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Убирает cache_control из блоков (для записи в историю — следующий turn пересчитает границу).
|
* Заглушка для обратной совместимости — убирает cache_control из блоков.
|
||||||
|
* В Groq-режиме cache_control не используется, функция — no-op.
|
||||||
*/
|
*/
|
||||||
export function stripCacheControl(content: any): any {
|
export function stripCacheControl(content: any): any {
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
@@ -76,33 +80,9 @@ export function stripCacheControl(content: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Граница prompt-кеша: всё, кроме последних N сообщений, помечаем cache_control
|
* Заглушка для обратной совместимости.
|
||||||
* на последнем блоке последнего «старого» сообщения. Даёт cache hit на каждом turn.
|
* В Groq-режиме prompt-кеширование не используется — просто возвращаем историю как есть.
|
||||||
*/
|
*/
|
||||||
export function buildMessagesWithCache(history: HistoryMessage[], cacheTailUncached = 2): HistoryMessage[] {
|
export function buildMessagesWithCache(history: HistoryMessage[], _cacheTailUncached = 2): HistoryMessage[] {
|
||||||
if (history.length <= cacheTailUncached) {
|
|
||||||
return history.map((m) => ({ role: m.role, content: m.content }))
|
return history.map((m) => ({ role: m.role, content: m.content }))
|
||||||
}
|
}
|
||||||
const cacheBoundary = history.length - cacheTailUncached
|
|
||||||
return history.map((msg, i) => {
|
|
||||||
if (i === cacheBoundary - 1) {
|
|
||||||
return { role: msg.role, content: wrapLastBlockWithCache(msg.content) }
|
|
||||||
}
|
|
||||||
return { role: msg.role, content: msg.content }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapLastBlockWithCache(content: any): any {
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
return [{ type: 'text', text: content, cache_control: { type: 'ephemeral' } }]
|
|
||||||
}
|
|
||||||
if (Array.isArray(content) && content.length) {
|
|
||||||
const out = [...content]
|
|
||||||
const last = out[out.length - 1]
|
|
||||||
if (last && typeof last === 'object') {
|
|
||||||
out[out.length - 1] = { ...last, cache_control: { type: 'ephemeral' } }
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
/**
|
/**
|
||||||
* Tool schemas для Anthropic API. Порт TOOL_SCHEMAS из satellite/tools.py.
|
* Tool schemas для Groq API (OpenAI-compatible format).
|
||||||
* Формат — Anthropic native tools (name + description + input_schema).
|
* Было: Anthropic.Tool[] (input_schema) → Стало: Groq/OpenAI function tools (parameters).
|
||||||
*/
|
*/
|
||||||
import type Anthropic from '@anthropic-ai/sdk'
|
|
||||||
|
|
||||||
export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
export interface GroqTool {
|
||||||
|
type: 'function'
|
||||||
|
function: {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
parameters: {
|
||||||
|
type: string
|
||||||
|
properties: Record<string, any>
|
||||||
|
required?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOOL_SCHEMAS: GroqTool[] = [
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'get_weather',
|
name: 'get_weather',
|
||||||
description:
|
description:
|
||||||
'Получить текущую погоду и короткий прогноз для города. ' +
|
'Получить текущую погоду и короткий прогноз для города. ' +
|
||||||
'Для вопросов вроде «какая сегодня погода», «холодно ли на улице», «нужен ли зонт». ' +
|
'Для вопросов вроде «какая сегодня погода», «холодно ли на улице», «нужен ли зонт». ' +
|
||||||
'По умолчанию — Санкт-Петербург.',
|
'По умолчанию — Санкт-Петербург.',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
city: {
|
city: {
|
||||||
@@ -23,12 +37,15 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'get_transport',
|
name: 'get_transport',
|
||||||
description:
|
description:
|
||||||
'Расписание ближайших трамваев на остановке Ул. Антонова-Овсеенко. ' +
|
'Расписание ближайших трамваев на остановке Ул. Антонова-Овсеенко. ' +
|
||||||
'Для вопросов «когда следующий 23-й», «что ближайшее в центр», «пора идти на остановку».',
|
'Для вопросов «когда следующий 23-й», «что ближайшее в центр», «пора идти на остановку».',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
direction: {
|
direction: {
|
||||||
@@ -46,13 +63,16 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'get_today_events',
|
name: 'get_today_events',
|
||||||
description:
|
description:
|
||||||
'События из календаря (Даниил + Света). Вернёт id события, title, start, end, ' +
|
'События из календаря (Даниил + Света). Вернёт id события, title, start, end, ' +
|
||||||
'owner («daniil» или «sveta»). ВАЖНО: для update_event / delete_event сначала ' +
|
'owner («daniil» или «sveta»). ВАЖНО: для update_event / delete_event сначала ' +
|
||||||
'вызывай этот tool чтобы получить event_id.',
|
'вызывай этот tool чтобы получить event_id.',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
range: {
|
range: {
|
||||||
@@ -63,13 +83,16 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'create_event',
|
name: 'create_event',
|
||||||
description:
|
description:
|
||||||
'Создать событие в Google Calendar. ВАЖНО: параметр owner обязателен. ' +
|
'Создать событие в Google Calendar. ВАЖНО: параметр owner обязателен. ' +
|
||||||
'Если пользователь не сказал чей это календарь — СПРОСИ у него ' +
|
'Если пользователь не сказал чей это календарь — СПРОСИ у него ' +
|
||||||
'(«в твой календарь или в Светин?») и только потом вызывай tool. Не угадывай.',
|
'(«в твой календарь или в Светин?») и только потом вызывай tool. Не угадывай.',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: 'string', description: 'Название события' },
|
title: { type: 'string', description: 'Название события' },
|
||||||
@@ -95,13 +118,16 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['title', 'date', 'owner'],
|
required: ['title', 'date', 'owner'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'update_event',
|
name: 'update_event',
|
||||||
description:
|
description:
|
||||||
'Изменить существующее событие. Сначала обязательно вызови get_today_events ' +
|
'Изменить существующее событие. Сначала обязательно вызови get_today_events ' +
|
||||||
'чтобы получить event_id и owner нужного события. Передавай только те поля ' +
|
'чтобы получить event_id и owner нужного события. Передавай только те поля ' +
|
||||||
'которые меняешь.',
|
'которые меняешь.',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
event_id: { type: 'string' },
|
event_id: { type: 'string' },
|
||||||
@@ -119,13 +145,16 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['event_id', 'owner'],
|
required: ['event_id', 'owner'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'delete_event',
|
name: 'delete_event',
|
||||||
description:
|
description:
|
||||||
'Удалить событие из календаря. Сначала вызови get_today_events чтобы найти ' +
|
'Удалить событие из календаря. Сначала вызови get_today_events чтобы найти ' +
|
||||||
'event_id и определить owner. Подтверди удаление с пользователем если событие ' +
|
'event_id и определить owner. Подтверди удаление с пользователем если событие ' +
|
||||||
'важное (встреча, врач, работа).',
|
'важное (встреча, врач, работа).',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
event_id: { type: 'string' },
|
event_id: { type: 'string' },
|
||||||
@@ -134,20 +163,29 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['event_id', 'owner'],
|
required: ['event_id', 'owner'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'get_notes',
|
name: 'get_notes',
|
||||||
description:
|
description:
|
||||||
'Список заметок и списков покупок с планшета. Для «что мне купить», ' +
|
'Список заметок и списков покупок с планшета. Для «что мне купить», ' +
|
||||||
'«что в списке», «какие записи».',
|
'«что в списке», «какие записи».',
|
||||||
input_schema: { type: 'object', properties: {} },
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'set_timer',
|
name: 'set_timer',
|
||||||
description:
|
description:
|
||||||
'Запустить таймер на планшете. Показывает обратный отсчёт с названием и звенит ' +
|
'Запустить таймер на планшете. Показывает обратный отсчёт с названием и звенит ' +
|
||||||
'по окончании. Используй для «поставь таймер на 10 минут», «напомни через час», ' +
|
'по окончании. Используй для «поставь таймер на 10 минут», «напомни через час», ' +
|
||||||
'«засеки 5 минут для чайника».',
|
'«засеки 5 минут для чайника».',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
seconds: {
|
seconds: {
|
||||||
@@ -164,12 +202,15 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['seconds', 'label'],
|
required: ['seconds', 'label'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'cancel_timer',
|
name: 'cancel_timer',
|
||||||
description:
|
description:
|
||||||
'Отменить активный таймер по его названию. Для «отмени таймер чайник», ' +
|
'Отменить активный таймер по его названию. Для «отмени таймер чайник», ' +
|
||||||
'«убери таймер пасты», «останови отсчёт».',
|
'«убери таймер пасты», «останови отсчёт».',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
label: {
|
label: {
|
||||||
@@ -180,12 +221,15 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['label'],
|
required: ['label'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
name: 'adjust_timer',
|
name: 'adjust_timer',
|
||||||
description:
|
description:
|
||||||
'Изменить оставшееся время таймера. Для «добавь ещё 5 минут», «убавь на минуту», ' +
|
'Изменить оставшееся время таймера. Для «добавь ещё 5 минут», «убавь на минуту», ' +
|
||||||
'«накинь времени чайнику». Положительный delta_seconds = добавить, отрицательный = уменьшить.',
|
'«накинь времени чайнику». Положительный delta_seconds = добавить, отрицательный = уменьшить.',
|
||||||
input_schema: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
label: {
|
label: {
|
||||||
@@ -200,4 +244,5 @@ export const TOOL_SCHEMAS: Anthropic.Tool[] = [
|
|||||||
required: ['label', 'delta_seconds'],
|
required: ['label', 'delta_seconds'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user