feat: add robust LLM JSON parser and fix Group Chat schema (#388)

Add robust LLM JSON parsing utilities to handle unreliable model output:
- Parse tool arguments with tolerance for Python format (single quotes, trailing commas)
- Extract text from Anthropic-style content arrays in streaming events
- Normalize tool_result content to string format per Hermes spec
- Parse message.delta and run.completed output to avoid displaying JSON strings

Fix Group Chat database schema errors:
- Add id column as PRIMARY KEY to gc_room_agents and gc_room_members tables
- Change from composite primary keys to single-column id keys
- Update tests to match new schema structure

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-02 08:58:14 +08:00
committed by GitHub
parent 9325aa5482
commit 969c7c0e1a
5 changed files with 457 additions and 135 deletions
+10 -9
View File
@@ -65,31 +65,32 @@ describe('Hermes schema initialization', () => {
expect(cols.some(c => c.name === 'output_tokens')).toBe(true)
})
it('handles composite primary key tables correctly', async () => {
it('handles single-column primary key tables correctly', async () => {
const { initAllHermesTables, GC_ROOM_AGENTS_TABLE } =
await import('../../packages/server/src/db/hermes/schemas')
expect(() => initAllHermesTables()).not.toThrow()
// Verify composite primary key
// Verify table has primary key and required columns
const tableInfo = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`).get(GC_ROOM_AGENTS_TABLE) as { sql: string }
expect(tableInfo.sql).toContain('PRIMARY KEY')
expect(tableInfo.sql).toContain('id')
expect(tableInfo.sql).toContain('roomId')
expect(tableInfo.sql).toContain('agentId')
// Verify we can insert with same roomId but different agentId
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?)`)
.run('room-1', 'agent-1', 'default', 'Agent 1', '', 0)
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?)`)
.run('room-1', 'agent-2', 'default', 'Agent 2', '', 0)
// Verify we can insert multiple entries with unique id
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run('agent-1', 'room-1', 'agent-1', 'default', 'Agent 1', '', 0)
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run('agent-2', 'room-1', 'agent-2', 'default', 'Agent 2', '', 0)
const count = db.prepare(`SELECT COUNT(*) as count FROM "${GC_ROOM_AGENTS_TABLE}"`).get() as { count: number }
expect(count.count).toBe(2)
// Verify duplicate primary key is rejected
expect(() => {
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?)`)
.run('room-1', 'agent-1', 'default', 'Agent 1 Duplicate', '', 0)
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run('agent-1', 'room-1', 'agent-1', 'default', 'Agent 1 Duplicate', '', 0)
}).toThrow()
})
})