Skip to content

Commit 5800222

Browse files
committed
feat: service discovery with scene merge and internalized pagination
1 parent 95708cc commit 5800222

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** Retrieval-service discovery: per-scene queries merged into one model-facing list; pagination stays internal. */
2+
3+
import type { ServiceListResponse } from './api-types.js'
4+
import type { KbClient } from './client.js'
5+
import { KB_PATHS } from './endpoints.js'
6+
7+
/** Server page-size maximum; one page per scene covers ordinary workspaces. */
8+
const MAX_PAGE_SIZE = 100
9+
10+
export interface ServiceEntry {
11+
agent_id: string
12+
name: string
13+
scene: string
14+
status: string
15+
knowledge_bases: string[]
16+
}
17+
18+
export interface ServiceList {
19+
services: ServiceEntry[]
20+
total: number
21+
/** True when some scene reported more rows than one max page returned. */
22+
truncated: boolean
23+
}
24+
25+
export interface ListServicesQuery {
26+
scene?: 'chat' | 'search'
27+
nameFilter?: string
28+
}
29+
30+
/**
31+
* List retrieval/Q&A services. An omitted scene fans out to both scenes and merges.
32+
* @param client - the shared knowledge API client.
33+
* @param query - optional scene and fuzzy name filter.
34+
* @returns merged entries, the server-reported total, and the truncation flag.
35+
*/
36+
export async function listServices(client: KbClient, query: ListServicesQuery): Promise<ServiceList> {
37+
const scenes: ('chat' | 'search')[] = query.scene ? [query.scene] : ['chat', 'search']
38+
const services: ServiceEntry[] = []
39+
let total = 0
40+
for (const scene of scenes) {
41+
const res = await client.postJson<ServiceListResponse>(KB_PATHS.serviceList, {
42+
agent_scene: scene,
43+
...(query.nameFilter ? { agent_name: query.nameFilter } : {}),
44+
page_number: 1,
45+
page_size: MAX_PAGE_SIZE,
46+
})
47+
total += res.data?.total_count ?? 0
48+
for (const row of res.data?.rows ?? []) {
49+
services.push({
50+
agent_id: row.agent_id ?? '',
51+
name: row.agent_name ?? '',
52+
scene: row.agent_scene ?? scene,
53+
status: row.agent_status ?? '',
54+
knowledge_bases: (row.pipeline_list ?? []).map(p => p.pipeline_name ?? p.pipeline_id ?? '').filter(Boolean),
55+
})
56+
}
57+
}
58+
return { services, total, truncated: total > services.length }
59+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import type { ServiceListResponse } from '../src/api-types.js'
3+
import type { KbClient } from '../src/client.js'
4+
import { listServices } from '../src/services.js'
5+
6+
function fakeClient(byScene: Record<string, ServiceListResponse>) {
7+
const postJson = vi.fn(async (_path: string, body: { agent_scene: string }) => byScene[body.agent_scene])
8+
return { client: { postJson } as unknown as KbClient, postJson }
9+
}
10+
11+
const row = (id: string, scene: string) => ({
12+
agent_id: id, agent_name: `svc-${id}`, agent_scene: scene, agent_status: 'deployed',
13+
pipeline_list: [{ pipeline_id: 'p1', pipeline_name: 'kb-one' }],
14+
})
15+
16+
describe('listServices', () => {
17+
it('queries both scenes when scene is omitted and merges rows with scene tags', async () => {
18+
const { client, postJson } = fakeClient({
19+
chat: { data: { total_count: 1, rows: [row('a', 'chat')] } },
20+
search: { data: { total_count: 1, rows: [row('b', 'search')] } },
21+
})
22+
const out = await listServices(client, {})
23+
expect(postJson).toHaveBeenCalledTimes(2)
24+
expect(out.services.map(s => [s.agent_id, s.scene])).toEqual([['a', 'chat'], ['b', 'search']])
25+
expect(out.services[0]!.knowledge_bases).toEqual(['kb-one'])
26+
expect(out.total).toBe(2)
27+
expect(out.truncated).toBe(false)
28+
const body = postJson.mock.calls[0]![1] as unknown as Record<string, unknown>
29+
expect(body.page_number).toBe(1)
30+
expect(body.page_size).toBe(100)
31+
})
32+
33+
it('queries one scene and forwards the name filter', async () => {
34+
const { client, postJson } = fakeClient({ search: { data: { total_count: 0, rows: [] } } })
35+
await listServices(client, { scene: 'search', nameFilter: '客服' })
36+
expect(postJson).toHaveBeenCalledTimes(1)
37+
expect((postJson.mock.calls[0]![1] as unknown as Record<string, unknown>).agent_name).toBe('客服')
38+
})
39+
40+
it('flags truncation when a scene exceeds one max page', async () => {
41+
const { client } = fakeClient({
42+
chat: { data: { total_count: 250, rows: [row('a', 'chat')] } },
43+
search: { data: { total_count: 0, rows: [] } },
44+
})
45+
const out = await listServices(client, {})
46+
expect(out.truncated).toBe(true)
47+
expect(out.total).toBe(250)
48+
})
49+
})

0 commit comments

Comments
 (0)