@agentxv2/gateway 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +53 -0
- package/db/migrations/001_init.sql +117 -0
- package/db/migrations/002_agents.sql +20 -0
- package/db/migrations/003_a2a_results.sql +23 -0
- package/deploy/.env.deploy.example +23 -0
- package/deploy/add_swap_build.py +45 -0
- package/deploy/build_and_deploy.py +110 -0
- package/deploy/build_deploy_a2a.py +190 -0
- package/deploy/build_no_lint.py +52 -0
- package/deploy/build_on_fe.py +86 -0
- package/deploy/build_on_gw.py +98 -0
- package/deploy/build_prod.py +54 -0
- package/deploy/check_bal.py +31 -0
- package/deploy/check_code.py +47 -0
- package/deploy/check_deploy.sh +20 -0
- package/deploy/check_docs.py +16 -0
- package/deploy/check_frontend.py +42 -0
- package/deploy/check_frontend2.py +35 -0
- package/deploy/check_pks.py +34 -0
- package/deploy/check_status.py +32 -0
- package/deploy/check_v2.py +8 -0
- package/deploy/clean_install.py +28 -0
- package/deploy/deploy_a2a.py +58 -0
- package/deploy/deploy_a2a.sh +33 -0
- package/deploy/deploy_a2a_bg.sh +40 -0
- package/deploy/deploy_a2a_v2.py +151 -0
- package/deploy/deploy_a2a_v2.sh +36 -0
- package/deploy/deploy_bin.py +64 -0
- package/deploy/deploy_config.py +34 -0
- package/deploy/deploy_final.py +42 -0
- package/deploy/deploy_frontend.py +108 -0
- package/deploy/deploy_mcp.py +108 -0
- package/deploy/deploy_now.py +49 -0
- package/deploy/deploy_push_all.py +128 -0
- package/deploy/deploy_script.py +64 -0
- package/deploy/deploy_sep_a2a.sh +18 -0
- package/deploy/dev_mode.py +51 -0
- package/deploy/diag.py +12 -0
- package/deploy/diff_test.py +22 -0
- package/deploy/final_check.py +33 -0
- package/deploy/final_deploy.py +50 -0
- package/deploy/final_fix.py +54 -0
- package/deploy/final_push.py +114 -0
- package/deploy/find_frontend.py +38 -0
- package/deploy/fix_a2a.py +43 -0
- package/deploy/fix_and_build.py +77 -0
- package/deploy/fix_and_verify.py +56 -0
- package/deploy/fix_build.py +51 -0
- package/deploy/forge_deploy.py +67 -0
- package/deploy/full_push.py +101 -0
- package/deploy/full_push2.py +106 -0
- package/deploy/go_prod.py +59 -0
- package/deploy/gw_deploy.py +133 -0
- package/deploy/gw_final.py +206 -0
- package/deploy/gw_restart.py +28 -0
- package/deploy/gw_status.py +18 -0
- package/deploy/inline_build.py +99 -0
- package/deploy/one_shot.py +84 -0
- package/deploy/ox_deploy.py +32 -0
- package/deploy/pg_setup.py +36 -0
- package/deploy/prod_deploy.py +126 -0
- package/deploy/prod_final.py +140 -0
- package/deploy/prod_go.py +61 -0
- package/deploy/prod_one_shot.py +139 -0
- package/deploy/prod_setup.sh +62 -0
- package/deploy/prod_v2.py +91 -0
- package/deploy/prod_v3.py +103 -0
- package/deploy/push_docs.py +93 -0
- package/deploy/push_mcp.py +66 -0
- package/deploy/push_mcp_doc.py +28 -0
- package/deploy/push_v063.py +80 -0
- package/deploy/push_v2.py +71 -0
- package/deploy/qcheck.py +13 -0
- package/deploy/qcheck2.py +18 -0
- package/deploy/quick_check.py +8 -0
- package/deploy/rebuild.py +47 -0
- package/deploy/rebuild_frontend.py +44 -0
- package/deploy/restart.py +28 -0
- package/deploy/restore_frontend.py +42 -0
- package/deploy/restore_frontend2.py +41 -0
- package/deploy/restore_gateway.py +34 -0
- package/deploy/restore_quick.py +33 -0
- package/deploy/run_bash.py +19 -0
- package/deploy/run_sep.py +20 -0
- package/deploy/sdk_upgrade.py +50 -0
- package/deploy/server_prepare.py +41 -0
- package/deploy/server_setup.py +41 -0
- package/deploy/ssh_check.py +14 -0
- package/deploy/ssh_push.py +24 -0
- package/deploy/start_deploy.py +35 -0
- package/deploy/start_frontend.py +12 -0
- package/deploy/start_v2.py +29 -0
- package/deploy/stat.py +18 -0
- package/deploy/tail_log.py +11 -0
- package/deploy/update_env.py +32 -0
- package/deploy/upload_and_start.py +107 -0
- package/deploy/verify_a2a.py +13 -0
- package/deploy/verify_mcp.py +37 -0
- package/deploy/verify_sdk.py +21 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/crypto.d.ts +2 -0
- package/dist/lib/crypto.js +43 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/db.d.ts +3 -0
- package/dist/lib/db.js +26 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/ethers-utils.d.ts +60 -0
- package/dist/lib/ethers-utils.js +186 -0
- package/dist/lib/ethers-utils.js.map +1 -0
- package/dist/middleware/adminAuth.d.ts +2 -0
- package/dist/middleware/adminAuth.js +31 -0
- package/dist/middleware/adminAuth.js.map +1 -0
- package/dist/middleware/auth.d.ts +22 -0
- package/dist/middleware/auth.js +192 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +36 -0
- package/dist/middleware/error-handler.js +100 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +107 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/routes/a2a.d.ts +2 -0
- package/dist/routes/a2a.js +84 -0
- package/dist/routes/a2a.js.map +1 -0
- package/dist/routes/admin.d.ts +2 -0
- package/dist/routes/admin.js +180 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/agents.d.ts +2 -0
- package/dist/routes/agents.js +43 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/chat.d.ts +2 -0
- package/dist/routes/chat.js +183 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/history.d.ts +2 -0
- package/dist/routes/history.js +92 -0
- package/dist/routes/history.js.map +1 -0
- package/dist/routes/mcp.d.ts +13 -0
- package/dist/routes/mcp.js +513 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/tenant.d.ts +2 -0
- package/dist/routes/tenant.js +154 -0
- package/dist/routes/tenant.js.map +1 -0
- package/dist/services/a2a-worker.d.ts +7 -0
- package/dist/services/a2a-worker.js +406 -0
- package/dist/services/a2a-worker.js.map +1 -0
- package/dist/services/agent-indexer.d.ts +4 -0
- package/dist/services/agent-indexer.js +124 -0
- package/dist/services/agent-indexer.js.map +1 -0
- package/e2e_wallet.js +175 -0
- package/ecosystem.config.js +17 -0
- package/package.json +33 -0
- package/src/config.ts +47 -0
- package/src/index.ts +143 -0
- package/src/lib/crypto.ts +44 -0
- package/src/lib/db.ts +25 -0
- package/src/lib/ethers-utils.ts +229 -0
- package/src/middleware/adminAuth.ts +34 -0
- package/src/middleware/auth.ts +244 -0
- package/src/middleware/error-handler.ts +109 -0
- package/src/middleware/rate-limiter.ts +115 -0
- package/src/routes/a2a.ts +97 -0
- package/src/routes/admin.ts +219 -0
- package/src/routes/agents.ts +52 -0
- package/src/routes/chat.ts +219 -0
- package/src/routes/history.ts +114 -0
- package/src/routes/mcp.ts +567 -0
- package/src/routes/tenant.ts +196 -0
- package/src/services/a2a-worker.ts +486 -0
- package/src/services/agent-indexer.ts +127 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// AgentX Gateway — Tenant Management Routes
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { Router, Request, Response } from 'express'
|
|
6
|
+
import { getPool } from '../lib/db'
|
|
7
|
+
import { encryptApiKey, decryptApiKey } from '../lib/crypto'
|
|
8
|
+
import { config } from '../config'
|
|
9
|
+
|
|
10
|
+
const router = Router()
|
|
11
|
+
|
|
12
|
+
// GET /api/v1/tenant/me
|
|
13
|
+
router.get('/me', async (req: Request, res: Response) => {
|
|
14
|
+
const tenant = req.tenant
|
|
15
|
+
|
|
16
|
+
const pool = getPool()
|
|
17
|
+
const [planRow, keysRow, usageRow] = await Promise.all([
|
|
18
|
+
pool.query(`SELECT name, slug, quota_daily, quota_monthly, platform_models, byok_enabled, rate_limit_rpm, max_concurrent, features FROM plans WHERE id = $1`, [tenant!.planId]),
|
|
19
|
+
pool.query(`SELECT id, provider, model, label, endpoint, is_active, last_validated, created_at FROM tenant_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC`, [tenant!.id]),
|
|
20
|
+
pool.query(`SELECT COALESCE(SUM(tokens_total), 0) as total_tokens, COALESCE(SUM(tool_calls), 0) as total_tool_calls FROM usage_logs WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '24 hours'`, [tenant!.id]),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const platformModels = planRow.rows[0] ? planRow.rows[0].platform_models : []
|
|
24
|
+
const ownKeys = keysRow.rows.map(r => ({
|
|
25
|
+
id: r.id,
|
|
26
|
+
provider: r.provider,
|
|
27
|
+
model: r.model,
|
|
28
|
+
label: r.label,
|
|
29
|
+
endpoint: r.endpoint,
|
|
30
|
+
is_active: r.is_active,
|
|
31
|
+
last_validated: r.last_validated,
|
|
32
|
+
created_at: r.created_at,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
res.json({
|
|
36
|
+
tenant: {
|
|
37
|
+
id: tenant!.id,
|
|
38
|
+
wallet_address: tenant!.walletAddress,
|
|
39
|
+
status: tenant!.status,
|
|
40
|
+
},
|
|
41
|
+
plan: planRow.rows[0] ? {
|
|
42
|
+
name: planRow.rows[0].name,
|
|
43
|
+
slug: planRow.rows[0].slug,
|
|
44
|
+
quota_daily: planRow.rows[0].quota_daily,
|
|
45
|
+
quota_used: tenant!.quotaUsed,
|
|
46
|
+
platform_models: platformModels,
|
|
47
|
+
byok_enabled: planRow.rows[0].byok_enabled,
|
|
48
|
+
rate_limit_rpm: planRow.rows[0].rate_limit_rpm,
|
|
49
|
+
max_concurrent: planRow.rows[0].max_concurrent,
|
|
50
|
+
} : null,
|
|
51
|
+
own_keys: ownKeys,
|
|
52
|
+
usage_today: {
|
|
53
|
+
total_tokens: parseInt(usageRow.rows[0]?.total_tokens || '0', 10),
|
|
54
|
+
total_tool_calls: parseInt(usageRow.rows[0]?.total_tool_calls || '0', 10),
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// GET /api/v1/tenant/keys
|
|
60
|
+
router.get('/keys', async (req: Request, res: Response) => {
|
|
61
|
+
const pool = getPool()
|
|
62
|
+
const result = await pool.query(
|
|
63
|
+
`SELECT id, provider, model, label, endpoint, is_active, last_validated, created_at FROM tenant_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC`,
|
|
64
|
+
[req.tenant!.id]
|
|
65
|
+
)
|
|
66
|
+
res.json({ keys: result.rows })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// POST /api/v1/tenant/keys
|
|
70
|
+
router.post('/keys', async (req: Request, res: Response) => {
|
|
71
|
+
const { provider, endpoint, api_key, model, label } = req.body
|
|
72
|
+
|
|
73
|
+
if (!provider || !endpoint || !api_key || !model) {
|
|
74
|
+
res.status(400).json({ error: 'provider, endpoint, api_key, and model are required' })
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const encrypted = encryptApiKey(api_key, config.masterEncryptionKey)
|
|
79
|
+
|
|
80
|
+
const pool = getPool()
|
|
81
|
+
const result = await pool.query(
|
|
82
|
+
`INSERT INTO tenant_api_keys (tenant_id, provider, endpoint, api_key, model, label)
|
|
83
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
84
|
+
RETURNING id, provider, model, label, endpoint, is_active, created_at`,
|
|
85
|
+
[req.tenant!.id, provider, endpoint, encrypted, model, label || null]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
res.status(201).json({ key: result.rows[0] })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// DELETE /api/v1/tenant/keys/:keyId
|
|
92
|
+
router.delete('/keys/:keyId', async (req: Request, res: Response) => {
|
|
93
|
+
const pool = getPool()
|
|
94
|
+
const result = await pool.query(
|
|
95
|
+
`DELETE FROM tenant_api_keys WHERE id = $1 AND tenant_id = $2 RETURNING id`,
|
|
96
|
+
[req.params.keyId, req.tenant!.id]
|
|
97
|
+
)
|
|
98
|
+
if (result.rows.length === 0) {
|
|
99
|
+
res.status(404).json({ error: 'Key not found' })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
res.json({ deleted: true })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// POST /api/v1/tenant/keys/:keyId/validate
|
|
106
|
+
router.post('/keys/:keyId/validate', async (req: Request, res: Response) => {
|
|
107
|
+
const pool = getPool()
|
|
108
|
+
const keyRow = await pool.query(
|
|
109
|
+
`SELECT * FROM tenant_api_keys WHERE id = $1 AND tenant_id = $2`,
|
|
110
|
+
[req.params.keyId, req.tenant!.id]
|
|
111
|
+
)
|
|
112
|
+
if (keyRow.rows.length === 0) {
|
|
113
|
+
res.status(404).json({ error: 'Key not found' })
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tk = keyRow.rows[0]
|
|
118
|
+
const key = decryptApiKey(tk.api_key, config.masterEncryptionKey)
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const testRes = await fetch(`${tk.endpoint}/models`, {
|
|
122
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
123
|
+
})
|
|
124
|
+
if (testRes.ok) {
|
|
125
|
+
await pool.query(
|
|
126
|
+
`UPDATE tenant_api_keys SET is_active = true, last_validated = NOW() WHERE id = $1`,
|
|
127
|
+
[tk.id]
|
|
128
|
+
)
|
|
129
|
+
res.json({ valid: true, provider: tk.provider, endpoint: tk.endpoint })
|
|
130
|
+
} else {
|
|
131
|
+
await pool.query(
|
|
132
|
+
`UPDATE tenant_api_keys SET is_active = false WHERE id = $1`,
|
|
133
|
+
[tk.id]
|
|
134
|
+
)
|
|
135
|
+
res.json({ valid: false, error: `HTTP ${testRes.status}` })
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
res.json({ valid: false, error: String(err) })
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// GET /api/v1/tenant/usage
|
|
143
|
+
router.get('/usage', async (req: Request, res: Response) => {
|
|
144
|
+
const days = parseInt(req.query.days as string || '30', 10)
|
|
145
|
+
const pool = getPool()
|
|
146
|
+
|
|
147
|
+
const [summaryRow, timelineRows] = await Promise.all([
|
|
148
|
+
pool.query(
|
|
149
|
+
`SELECT
|
|
150
|
+
key_source,
|
|
151
|
+
COALESCE(SUM(tokens_total), 0) as total_tokens,
|
|
152
|
+
COALESCE(SUM(tool_calls), 0) as total_tool_calls,
|
|
153
|
+
COUNT(*) as request_count
|
|
154
|
+
FROM usage_logs
|
|
155
|
+
WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '1 day' * $2
|
|
156
|
+
GROUP BY key_source`,
|
|
157
|
+
[req.tenant!.id, days]
|
|
158
|
+
),
|
|
159
|
+
pool.query(
|
|
160
|
+
`SELECT DATE(created_at) as day, key_source, COALESCE(SUM(tokens_total), 0) as tokens
|
|
161
|
+
FROM usage_logs
|
|
162
|
+
WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '1 day' * $2
|
|
163
|
+
GROUP BY DATE(created_at), key_source
|
|
164
|
+
ORDER BY day`,
|
|
165
|
+
[req.tenant!.id, days]
|
|
166
|
+
),
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
res.json({
|
|
170
|
+
summary: summaryRow.rows,
|
|
171
|
+
timeline: timelineRows.rows,
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// GET /api/v1/models
|
|
176
|
+
router.get('/models', async (req: Request, res: Response) => {
|
|
177
|
+
const tenant = req.tenant
|
|
178
|
+
|
|
179
|
+
const pool = getPool()
|
|
180
|
+
const [planRow, keysRow] = await Promise.all([
|
|
181
|
+
pool.query(`SELECT platform_models FROM plans WHERE id = $1`, [tenant!.planId]),
|
|
182
|
+
pool.query(`SELECT id, provider, model, label FROM tenant_api_keys WHERE tenant_id = $1 AND is_active = true`, [tenant!.id]),
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
res.json({
|
|
186
|
+
platform: planRow.rows[0]?.platform_models || [],
|
|
187
|
+
tenant_owned: keysRow.rows.map(r => ({
|
|
188
|
+
id: r.id,
|
|
189
|
+
provider: r.provider,
|
|
190
|
+
model: r.model,
|
|
191
|
+
label: r.label,
|
|
192
|
+
})),
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
export default router
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// AgentX Gateway — A2A Task Worker (v2 — ReAct AgentLoop + Multi-Agent Orchestration)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Background service that:
|
|
5
|
+
// 1. Polls OxaChain A2A contract for pending tasks (status=Created/Accepted)
|
|
6
|
+
// 2. Runs ReAct-style AgentLoop with A2A tools for each task
|
|
7
|
+
// 3. LLM can autonomously delegate sub-tasks to other agents via createTask()
|
|
8
|
+
// 4. Sub-tasks are processed inline and results fed back to the parent LLM
|
|
9
|
+
//
|
|
10
|
+
// Multi-Agent Orchestration Flow:
|
|
11
|
+
// Agent A's task → LLM(Agent A) analyzes
|
|
12
|
+
// → LLM decides: "I need Agent B for auditing"
|
|
13
|
+
// → calls agentx_a2a_create_task(Agent B, "audit", ...)
|
|
14
|
+
// → Worker submits tx on-chain, processes Agent B's task inline
|
|
15
|
+
// → Agent B's result fed back to Agent A's LLM
|
|
16
|
+
// → LLM(Agent A) continues: "Now I need Agent C to summarize"
|
|
17
|
+
// → calls agentx_a2a_create_task(Agent C, "summarize", ...)
|
|
18
|
+
// → Worker processes Agent C's task inline
|
|
19
|
+
// → Agent A's LLM aggregates all results → final output
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
import { ethers } from 'ethers'
|
|
23
|
+
import { getPool } from '../lib/db'
|
|
24
|
+
import { config } from '../config'
|
|
25
|
+
import { decryptApiKey } from '../lib/crypto'
|
|
26
|
+
|
|
27
|
+
// ── A2A Contract ABI ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const A2A_ABI = [
|
|
30
|
+
'function getAgentTasks(uint256 agentId) view returns (tuple(uint256,uint256,string,string,string,uint256,address,uint256,uint256,bytes32)[])',
|
|
31
|
+
'function getTask(uint256 taskId) view returns (uint256,uint256,string,string,string,uint256,address,uint256,uint256)',
|
|
32
|
+
'function createTask(uint256 agentId, string taskType, string inputData) returns (uint256)',
|
|
33
|
+
'event TaskCreated(uint256 indexed taskId, uint256 indexed agentId, address indexed client, string taskType, uint256 status)',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
interface A2ATaskOnChain {
|
|
39
|
+
taskId: number
|
|
40
|
+
agentId: number
|
|
41
|
+
taskType: string
|
|
42
|
+
inputData: string
|
|
43
|
+
outputData: string
|
|
44
|
+
status: number
|
|
45
|
+
clientAddress: string
|
|
46
|
+
createdAt: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AgentInfo {
|
|
50
|
+
id: number
|
|
51
|
+
name: string
|
|
52
|
+
owner: string
|
|
53
|
+
description: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Worker State ───────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
let pollingTimer: ReturnType<typeof setInterval> | null = null
|
|
59
|
+
let isRunning = false
|
|
60
|
+
const POLL_INTERVAL_MS = 30_000
|
|
61
|
+
const MAX_BATCH_SIZE = 3 // Reduced since each task may spawn sub-tasks
|
|
62
|
+
const MAX_DEPTH = 3 // Max delegation depth
|
|
63
|
+
const MAX_REACT_ITERATIONS = 5 // Max LLM tool-call rounds per task
|
|
64
|
+
const processingSet = new Set<number>()
|
|
65
|
+
let totalOrchestrated = 0 // Counter for multi-agent orchestrations
|
|
66
|
+
|
|
67
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export function startA2AWorker(): void {
|
|
70
|
+
if (pollingTimer) return
|
|
71
|
+
console.log('[A2A Worker v2] Starting ReAct AgentLoop mode, poll:', POLL_INTERVAL_MS / 1000, 's')
|
|
72
|
+
pollingTimer = setInterval(pollAndProcess, POLL_INTERVAL_MS)
|
|
73
|
+
setTimeout(pollAndProcess, 5_000)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function stopA2AWorker(): void {
|
|
77
|
+
if (pollingTimer) { clearInterval(pollingTimer); pollingTimer = null; console.log('[A2A Worker] Stopped') }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getWorkerStatus(): { running: boolean; processingSetSize: number; totalOrchestrated: number } {
|
|
81
|
+
return { running: pollingTimer !== null, processingSetSize: processingSet.size, totalOrchestrated }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Core Logic ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function pollAndProcess(): Promise<void> {
|
|
87
|
+
if (isRunning) { console.log('[A2A Worker] Previous poll still running, skipping'); return }
|
|
88
|
+
isRunning = true
|
|
89
|
+
try {
|
|
90
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
|
|
91
|
+
const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, provider)
|
|
92
|
+
const pool = getPool()
|
|
93
|
+
const { rows: agents } = await pool.query(`SELECT id, name, owner, description FROM agents ORDER BY id`)
|
|
94
|
+
if (agents.length === 0) { isRunning = false; return }
|
|
95
|
+
|
|
96
|
+
let processedCount = 0
|
|
97
|
+
for (const agent of agents) {
|
|
98
|
+
if (processedCount >= MAX_BATCH_SIZE) break
|
|
99
|
+
const agentId = agent.id as number
|
|
100
|
+
const agentName = agent.name as string
|
|
101
|
+
const agentOwner = (agent.owner as string || '').toLowerCase()
|
|
102
|
+
let tenantId: string | null = null
|
|
103
|
+
if (agentOwner) {
|
|
104
|
+
const { rows: tenants } = await pool.query(`SELECT id FROM tenants WHERE LOWER(wallet_address) = $1`, [agentOwner])
|
|
105
|
+
tenantId = tenants[0]?.id || null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { rows: existingResults } = await pool.query(
|
|
109
|
+
`SELECT task_id FROM a2a_task_results WHERE agent_id = $1 AND status = 2`, [agentId]
|
|
110
|
+
)
|
|
111
|
+
const completedTaskIds = new Set(existingResults.map((r: any) => r.task_id))
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const tasks: any[] = await a2aContract.getAgentTasks(agentId)
|
|
115
|
+
for (const task of tasks) {
|
|
116
|
+
if (processedCount >= MAX_BATCH_SIZE) break
|
|
117
|
+
const taskId = Number(task[0])
|
|
118
|
+
const status = Number(task[5])
|
|
119
|
+
if (status > 1 || completedTaskIds.has(taskId) || processingSet.has(taskId)) continue
|
|
120
|
+
|
|
121
|
+
const onChainTask: A2ATaskOnChain = {
|
|
122
|
+
taskId, agentId: Number(task[1]), taskType: String(task[2] || ''),
|
|
123
|
+
inputData: String(task[3] || ''), outputData: String(task[4] || ''),
|
|
124
|
+
status, clientAddress: String(task[6] || ''), createdAt: Number(task[7] || 0),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
processingSet.add(taskId); processedCount++
|
|
128
|
+
console.log(`[A2A Worker] Task #${taskId} → Agent #${agentId} (${agentName})`)
|
|
129
|
+
|
|
130
|
+
processTask(pool, onChainTask, { id: agentId, name: agentName, owner: agentOwner, description: agent.description as string }, tenantId, agents as AgentInfo[], 0)
|
|
131
|
+
.catch(err => console.error(`[A2A Worker] Task #${taskId} failed:`, err.message))
|
|
132
|
+
.finally(() => processingSet.delete(taskId))
|
|
133
|
+
}
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
if (!err.message?.includes('TaskNotFound') && !err.message?.includes('revert')) {
|
|
136
|
+
console.warn(`[A2A Worker] Error for agent #${agentId}:`, err.message?.slice(0, 100))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (processedCount > 0) console.log(`[A2A Worker] Queued ${processedCount} task(s)`)
|
|
141
|
+
} catch (err: any) { console.error('[A2A Worker] Poll error:', err.message) }
|
|
142
|
+
finally { isRunning = false }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── ReAct AgentLoop Task Processing ────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
const A2A_TOOLS = [
|
|
148
|
+
{
|
|
149
|
+
type: 'function' as const,
|
|
150
|
+
function: {
|
|
151
|
+
name: 'agentx_a2a_create_task',
|
|
152
|
+
description: 'DELEGATE work to another AgentX agent. Creates an on-chain task. Use this when you need another agent\'s expertise. The sub-task will be processed and its result returned to you.',
|
|
153
|
+
parameters: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
targetAgentId: { type: 'integer', description: 'The agent ID to delegate to' },
|
|
157
|
+
taskType: { type: 'string', description: 'Type of task (e.g., "audit", "analyze", "summarize", "translate")' },
|
|
158
|
+
inputData: { type: 'string', description: 'Full task description. Include ALL context the sub-agent needs.' },
|
|
159
|
+
},
|
|
160
|
+
required: ['targetAgentId', 'taskType', 'inputData'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'function' as const,
|
|
166
|
+
function: {
|
|
167
|
+
name: 'agentx_a2a_get_task',
|
|
168
|
+
description: 'Check the status and result of an A2A task by ID. Use to check if a delegated sub-task has completed.',
|
|
169
|
+
parameters: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: { taskId: { type: 'integer', description: 'The task ID to query' } },
|
|
172
|
+
required: ['taskId'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: 'function' as const,
|
|
178
|
+
function: {
|
|
179
|
+
name: 'agentx_list_agents',
|
|
180
|
+
description: 'List ALL available agents with their IDs, names, and descriptions. Use this to discover which agents can help with sub-tasks.',
|
|
181
|
+
parameters: { type: 'object', properties: {} },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
async function processTask(
|
|
187
|
+
pool: ReturnType<typeof getPool>,
|
|
188
|
+
task: A2ATaskOnChain,
|
|
189
|
+
agent: AgentInfo,
|
|
190
|
+
tenantId: string | null,
|
|
191
|
+
allAgents: AgentInfo[],
|
|
192
|
+
depth: number,
|
|
193
|
+
): Promise<string> {
|
|
194
|
+
const indent = ' '.repeat(depth)
|
|
195
|
+
const taskId = task.taskId
|
|
196
|
+
|
|
197
|
+
if (depth >= MAX_DEPTH) {
|
|
198
|
+
console.log(`${indent}[A2A] Max depth reached for task #${taskId}, processing directly`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await pool.query(
|
|
202
|
+
`INSERT INTO a2a_task_results (task_id, agent_id, task_type, input_data, tenant_id, status, created_at)
|
|
203
|
+
VALUES ($1, $2, $3, $4, $5, 1, NOW())
|
|
204
|
+
ON CONFLICT (task_id) DO UPDATE SET status = 1`,
|
|
205
|
+
[taskId, task.agentId, task.taskType, task.inputData, tenantId]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const llmConfig = await resolveLLMConfig(pool, tenantId)
|
|
210
|
+
if (!llmConfig) throw new Error('No LLM API key available')
|
|
211
|
+
|
|
212
|
+
console.log(`${indent}[A2A] ReAct loop for task #${taskId} (Agent: ${agent.name}, depth: ${depth})`)
|
|
213
|
+
|
|
214
|
+
// ── ReAct AgentLoop ─────────────────────────────────────────────────
|
|
215
|
+
const messages: { role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }[] = [
|
|
216
|
+
{
|
|
217
|
+
role: 'system',
|
|
218
|
+
content: buildOrchestrationSystemPrompt(agent, allAgents),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
role: 'user',
|
|
222
|
+
content: buildOrchestrationUserPrompt(task),
|
|
223
|
+
},
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
let finalContent = ''
|
|
227
|
+
let totalTokens = 0
|
|
228
|
+
let subTaskResults: { taskId: number; agentName: string; output: string }[] = []
|
|
229
|
+
|
|
230
|
+
for (let iter = 0; iter < MAX_REACT_ITERATIONS; iter++) {
|
|
231
|
+
const response = await callLLMWithTools(llmConfig, messages, A2A_TOOLS)
|
|
232
|
+
totalTokens += response.tokens
|
|
233
|
+
const choice = response.choices[0]
|
|
234
|
+
const msg = choice.message
|
|
235
|
+
|
|
236
|
+
// If no tool calls, we have the final answer
|
|
237
|
+
if (!msg.tool_calls || msg.tool_calls.length === 0) {
|
|
238
|
+
finalContent = msg.content || ''
|
|
239
|
+
messages.push({ role: 'assistant', content: finalContent })
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Process tool calls
|
|
244
|
+
messages.push({ role: 'assistant', content: msg.content || null, tool_calls: msg.tool_calls })
|
|
245
|
+
|
|
246
|
+
for (const tc of msg.tool_calls) {
|
|
247
|
+
const toolName = tc.function.name
|
|
248
|
+
let args: Record<string, unknown> = {}
|
|
249
|
+
try { args = JSON.parse(tc.function.arguments) } catch { /* raw string */ }
|
|
250
|
+
|
|
251
|
+
console.log(`${indent}[A2A] LLM calls ${toolName}(${JSON.stringify(args).slice(0, 80)})`)
|
|
252
|
+
|
|
253
|
+
let toolResult: string
|
|
254
|
+
|
|
255
|
+
switch (toolName) {
|
|
256
|
+
case 'agentx_list_agents': {
|
|
257
|
+
const list = allAgents.map(a => `#${a.id} "${a.name}" — ${a.description || '(no description)'}`).join('\n')
|
|
258
|
+
toolResult = `Available agents:\n${list}`
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'agentx_a2a_create_task': {
|
|
263
|
+
const targetId = Number(args.targetAgentId)
|
|
264
|
+
const subTaskType = String(args.taskType || 'analyze')
|
|
265
|
+
const subInput = String(args.inputData || '')
|
|
266
|
+
|
|
267
|
+
if (!targetId || isNaN(targetId)) {
|
|
268
|
+
toolResult = 'Error: targetAgentId is required'
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const targetAgent = allAgents.find(a => a.id === targetId)
|
|
273
|
+
const targetName = targetAgent?.name || `Agent #${targetId}`
|
|
274
|
+
|
|
275
|
+
// Submit createTask on-chain (permissionless — anyone can call)
|
|
276
|
+
let subTaskId: number
|
|
277
|
+
try {
|
|
278
|
+
subTaskId = await createTaskOnChain(targetId, subTaskType, subInput)
|
|
279
|
+
} catch (err: any) {
|
|
280
|
+
toolResult = `Error creating sub-task: ${err.message}`
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`${indent}[A2A] → Delegated to ${targetName} (#${targetId}), sub-task: #${subTaskId}`)
|
|
285
|
+
totalOrchestrated++
|
|
286
|
+
|
|
287
|
+
// Process sub-task recursively inline
|
|
288
|
+
const subOutput = await processTask(
|
|
289
|
+
pool,
|
|
290
|
+
{
|
|
291
|
+
taskId: subTaskId, agentId: targetId, taskType: subTaskType,
|
|
292
|
+
inputData: subInput, outputData: '', status: 0,
|
|
293
|
+
clientAddress: task.clientAddress, createdAt: Math.floor(Date.now() / 1000),
|
|
294
|
+
},
|
|
295
|
+
targetAgent || { id: targetId, name: targetName, owner: '', description: '' },
|
|
296
|
+
tenantId,
|
|
297
|
+
allAgents,
|
|
298
|
+
depth + 1,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
subTaskResults.push({ taskId: subTaskId, agentName: targetName, output: subOutput })
|
|
302
|
+
toolResult = `Sub-task #${subTaskId} completed by ${targetName}.\nResult: ${subOutput.slice(0, 2000)}`
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'agentx_a2a_get_task': {
|
|
307
|
+
const queryId = Number(args.taskId)
|
|
308
|
+
if (!queryId || isNaN(queryId)) { toolResult = 'Error: taskId required'; break }
|
|
309
|
+
try {
|
|
310
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
|
|
311
|
+
const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, provider)
|
|
312
|
+
const t = await a2aContract.getTask(queryId)
|
|
313
|
+
const statusMap = ['Created', 'Accepted', 'InProgress', 'Completed', 'Failed']
|
|
314
|
+
toolResult = JSON.stringify({
|
|
315
|
+
taskId: Number(t[0]), agentId: Number(t[1]), taskType: String(t[2]),
|
|
316
|
+
status: statusMap[Number(t[5])] || 'Unknown',
|
|
317
|
+
outputData: String(t[4] || ''),
|
|
318
|
+
})
|
|
319
|
+
} catch { toolResult = `Task #${queryId} not found` }
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
toolResult = `Unknown tool: ${toolName}`
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
messages.push({ role: 'tool', content: toolResult, tool_call_id: tc.id })
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!finalContent && messages.length > 2) {
|
|
332
|
+
// LLM used tools but didn't produce final text — get a summary
|
|
333
|
+
messages.push({ role: 'user', content: 'Based on all the sub-task results above, provide your final combined output. Return ONLY the final result.' })
|
|
334
|
+
const finalResp = await callLLMWithTools(llmConfig, messages, [])
|
|
335
|
+
finalContent = finalResp.choices[0]?.message?.content || 'Task processed via multi-agent orchestration.'
|
|
336
|
+
totalTokens += finalResp.tokens
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!finalContent) finalContent = 'Task processed.'
|
|
340
|
+
|
|
341
|
+
// Store result
|
|
342
|
+
await pool.query(
|
|
343
|
+
`UPDATE a2a_task_results SET status = 2, output_data = $2, llm_model = $3, tokens_used = $4, processed_at = NOW()
|
|
344
|
+
WHERE task_id = $1`,
|
|
345
|
+
[taskId, finalContent, llmConfig.model, totalTokens]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const orchestrationNote = subTaskResults.length > 0
|
|
349
|
+
? ` (orchestrated ${subTaskResults.length} sub-task(s): ${subTaskResults.map(s => `#${s.taskId}→${s.agentName}`).join(', ')})`
|
|
350
|
+
: ''
|
|
351
|
+
console.log(`${indent}[A2A] Task #${taskId} done, tokens: ${totalTokens}${orchestrationNote}`)
|
|
352
|
+
|
|
353
|
+
return finalContent
|
|
354
|
+
} catch (err: any) {
|
|
355
|
+
await pool.query(
|
|
356
|
+
`UPDATE a2a_task_results SET status = 3, error_message = $2, processed_at = NOW() WHERE task_id = $1`,
|
|
357
|
+
[taskId, err.message.slice(0, 500)]
|
|
358
|
+
)
|
|
359
|
+
console.error(`${' '.repeat(depth)}[A2A] Task #${taskId} failed:`, err.message.slice(0, 100))
|
|
360
|
+
throw err
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── On-Chain Helpers ──────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
async function createTaskOnChain(agentId: number, taskType: string, inputData: string): Promise<number> {
|
|
367
|
+
const workerPk = process.env.A2A_WORKER_PRIVATE_KEY
|
|
368
|
+
if (!workerPk) throw new Error('A2A_WORKER_PRIVATE_KEY not configured — cannot create sub-tasks')
|
|
369
|
+
|
|
370
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrlOxaChain)
|
|
371
|
+
const wallet = new ethers.Wallet(workerPk, provider)
|
|
372
|
+
const a2aContract = new ethers.Contract(config.a2aProtocolOxaChain, A2A_ABI, wallet)
|
|
373
|
+
|
|
374
|
+
const tx = await a2aContract.createTask(agentId, taskType, inputData)
|
|
375
|
+
const receipt = await tx.wait()
|
|
376
|
+
|
|
377
|
+
// Parse TaskCreated event to get taskId
|
|
378
|
+
for (const log of receipt.logs) {
|
|
379
|
+
if (log.topics.length >= 2) {
|
|
380
|
+
try { return Number(log.topics[1]) } catch { /* */ }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
throw new Error('Could not parse taskId from createTask tx')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── LLM Helpers ────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
interface LLMConfig {
|
|
389
|
+
endpoint: string
|
|
390
|
+
apiKey: string
|
|
391
|
+
model: string
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function resolveLLMConfig(pool: ReturnType<typeof getPool>, tenantId: string | null): Promise<LLMConfig | null> {
|
|
395
|
+
if (tenantId) {
|
|
396
|
+
const { rows } = await pool.query(
|
|
397
|
+
`SELECT * FROM tenant_api_keys WHERE tenant_id = $1 AND is_active = true ORDER BY random() LIMIT 1`, [tenantId]
|
|
398
|
+
)
|
|
399
|
+
if (rows.length > 0) return { endpoint: rows[0].endpoint, apiKey: decryptApiKey(rows[0].api_key, config.masterEncryptionKey), model: rows[0].model || 'deepseek-chat' }
|
|
400
|
+
}
|
|
401
|
+
const { rows: pks } = await pool.query(`SELECT * FROM platform_api_keys WHERE is_active = true ORDER BY random() LIMIT 1`)
|
|
402
|
+
if (pks.length > 0) return { endpoint: pks[0].endpoint, apiKey: decryptApiKey(pks[0].api_key, config.masterEncryptionKey), model: pks[0].models?.[0] || 'deepseek-chat' }
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildOrchestrationSystemPrompt(agent: AgentInfo, allAgents: AgentInfo[]): string {
|
|
407
|
+
const agentList = allAgents.slice(0, 30).map(a => ` #${a.id} "${a.name}" — ${a.description || 'No description'}`).join('\n')
|
|
408
|
+
|
|
409
|
+
return `You are "${agent.name}", an AI agent on the AgentX decentralized platform.
|
|
410
|
+
|
|
411
|
+
## Your Role
|
|
412
|
+
${agent.description || 'Process delegated tasks professionally and return results.'}
|
|
413
|
+
|
|
414
|
+
## Multi-Agent Orchestration
|
|
415
|
+
You have access to A2A (Agent-to-Agent) tools that let you DELEGATE work to other agents.
|
|
416
|
+
When a task is complex or requires specialized skills, break it down and delegate sub-tasks.
|
|
417
|
+
|
|
418
|
+
### Available Agents
|
|
419
|
+
${agentList}
|
|
420
|
+
|
|
421
|
+
### How to Orchestrate
|
|
422
|
+
1. Analyze the task — what parts need specialized agents?
|
|
423
|
+
2. Call agentx_list_agents to see available agents
|
|
424
|
+
3. Call agentx_a2a_create_task to delegate sub-tasks (each runs autonomously)
|
|
425
|
+
4. Call agentx_a2a_get_task to check sub-task results
|
|
426
|
+
5. Aggregate ALL sub-task results into your final output
|
|
427
|
+
|
|
428
|
+
### Rules
|
|
429
|
+
- Delegate when another agent has relevant expertise
|
|
430
|
+
- Include ALL context in inputData so the sub-agent can work independently
|
|
431
|
+
- After getting sub-task results, synthesize them into a coherent final answer
|
|
432
|
+
- If the task is simple and you can handle it alone, just answer directly
|
|
433
|
+
- Always provide a final answer (don't leave the user hanging)`
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function buildOrchestrationUserPrompt(task: A2ATaskOnChain): string {
|
|
437
|
+
return `Task Type: ${task.taskType}
|
|
438
|
+
Client: ${task.clientAddress}
|
|
439
|
+
|
|
440
|
+
Task Input:
|
|
441
|
+
${task.inputData || 'Please process this task.'}
|
|
442
|
+
|
|
443
|
+
Process this task. If it requires multiple specialized skills, use the available A2A tools to delegate sub-tasks to appropriate agents. Return your final combined output.`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── LLM Call with Tools (ReAct) ──────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
interface LLMToolCallResponse {
|
|
449
|
+
choices: { message: { role: string; content: string | null; tool_calls?: { id: string; function: { name: string; arguments: string } }[] } }[]
|
|
450
|
+
tokens: number
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function callLLMWithTools(
|
|
454
|
+
cfg: LLMConfig,
|
|
455
|
+
messages: { role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }[],
|
|
456
|
+
tools: typeof A2A_TOOLS,
|
|
457
|
+
): Promise<LLMToolCallResponse> {
|
|
458
|
+
const body: Record<string, unknown> = {
|
|
459
|
+
model: cfg.model,
|
|
460
|
+
messages,
|
|
461
|
+
max_tokens: 2048,
|
|
462
|
+
temperature: 0.3,
|
|
463
|
+
}
|
|
464
|
+
if (tools.length > 0) {
|
|
465
|
+
body.tools = tools
|
|
466
|
+
body.tool_choice = 'auto'
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const res = await fetch(`${cfg.endpoint}/chat/completions`, {
|
|
470
|
+
method: 'POST',
|
|
471
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
472
|
+
body: JSON.stringify(body),
|
|
473
|
+
signal: AbortSignal.timeout(120_000),
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
const errText = await res.text().catch(() => 'unknown error')
|
|
478
|
+
throw new Error(`LLM call failed (HTTP ${res.status}): ${errText.slice(0, 200)}`)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const data = await res.json()
|
|
482
|
+
return {
|
|
483
|
+
choices: data.choices || [],
|
|
484
|
+
tokens: data.usage?.total_tokens || 0,
|
|
485
|
+
}
|
|
486
|
+
}
|