mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add LLM integration module
- OllamaClient: ILLMClient implementation with tool support - System prompt and context builders for project overview - 18 tool definitions across 6 categories (read, edit, search, analysis, git, run) - XML response parser for tool call extraction - 98 new tests (419 total), 96.38% coverage
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
parseToolCalls,
|
||||
formatToolCallsAsXml,
|
||||
extractThinking,
|
||||
hasToolCalls,
|
||||
validateToolCallParams,
|
||||
} from "../../../../src/infrastructure/llm/ResponseParser.js"
|
||||
import { createToolCall } from "../../../../src/domain/value-objects/ToolCall.js"
|
||||
|
||||
describe("ResponseParser", () => {
|
||||
describe("parseToolCalls", () => {
|
||||
it("should parse a single tool call", () => {
|
||||
const response = `<tool_call name="get_lines">
|
||||
<path>src/index.ts</path>
|
||||
<start>1</start>
|
||||
<end>10</end>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls).toHaveLength(1)
|
||||
expect(result.toolCalls[0].name).toBe("get_lines")
|
||||
expect(result.toolCalls[0].params).toEqual({
|
||||
path: "src/index.ts",
|
||||
start: 1,
|
||||
end: 10,
|
||||
})
|
||||
expect(result.hasParseErrors).toBe(false)
|
||||
})
|
||||
|
||||
it("should parse multiple tool calls", () => {
|
||||
const response = `
|
||||
<tool_call name="get_lines">
|
||||
<path>src/a.ts</path>
|
||||
</tool_call>
|
||||
<tool_call name="get_function">
|
||||
<path>src/b.ts</path>
|
||||
<name>myFunc</name>
|
||||
</tool_call>
|
||||
`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls).toHaveLength(2)
|
||||
expect(result.toolCalls[0].name).toBe("get_lines")
|
||||
expect(result.toolCalls[1].name).toBe("get_function")
|
||||
})
|
||||
|
||||
it("should extract text content without tool calls", () => {
|
||||
const response = `Let me check the file.
|
||||
<tool_call name="get_lines">
|
||||
<path>src/index.ts</path>
|
||||
</tool_call>
|
||||
Here's what I found.`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.content).toContain("Let me check the file.")
|
||||
expect(result.content).toContain("Here's what I found.")
|
||||
expect(result.content).not.toContain("tool_call")
|
||||
})
|
||||
|
||||
it("should parse boolean values", () => {
|
||||
const response = `<tool_call name="git_diff">
|
||||
<staged>true</staged>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls[0].params.staged).toBe(true)
|
||||
})
|
||||
|
||||
it("should parse null values", () => {
|
||||
const response = `<tool_call name="test">
|
||||
<value>null</value>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls[0].params.value).toBe(null)
|
||||
})
|
||||
|
||||
it("should parse JSON arrays", () => {
|
||||
const response = `<tool_call name="git_commit">
|
||||
<files>["a.ts", "b.ts"]</files>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls[0].params.files).toEqual(["a.ts", "b.ts"])
|
||||
})
|
||||
|
||||
it("should parse JSON objects", () => {
|
||||
const response = `<tool_call name="test">
|
||||
<config>{"key": "value"}</config>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls[0].params.config).toEqual({ key: "value" })
|
||||
})
|
||||
|
||||
it("should return empty array for response without tool calls", () => {
|
||||
const response = "This is just a regular response."
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls).toHaveLength(0)
|
||||
expect(result.content).toBe(response)
|
||||
})
|
||||
|
||||
it("should handle named param syntax", () => {
|
||||
const response = `<tool_call name="get_lines">
|
||||
<param name="path">src/index.ts</param>
|
||||
<param name="start">5</param>
|
||||
</tool_call>`
|
||||
|
||||
const result = parseToolCalls(response)
|
||||
|
||||
expect(result.toolCalls[0].params).toEqual({
|
||||
path: "src/index.ts",
|
||||
start: 5,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatToolCallsAsXml", () => {
|
||||
it("should format tool calls as XML", () => {
|
||||
const toolCalls = [
|
||||
createToolCall("1", "get_lines", { path: "src/index.ts", start: 1 }),
|
||||
]
|
||||
|
||||
const xml = formatToolCallsAsXml(toolCalls)
|
||||
|
||||
expect(xml).toContain('<tool_call name="get_lines">')
|
||||
expect(xml).toContain("<path>src/index.ts</path>")
|
||||
expect(xml).toContain("<start>1</start>")
|
||||
expect(xml).toContain("</tool_call>")
|
||||
})
|
||||
|
||||
it("should format multiple tool calls", () => {
|
||||
const toolCalls = [
|
||||
createToolCall("1", "get_lines", { path: "a.ts" }),
|
||||
createToolCall("2", "get_function", { path: "b.ts", name: "foo" }),
|
||||
]
|
||||
|
||||
const xml = formatToolCallsAsXml(toolCalls)
|
||||
|
||||
expect(xml).toContain('<tool_call name="get_lines">')
|
||||
expect(xml).toContain('<tool_call name="get_function">')
|
||||
})
|
||||
|
||||
it("should handle object values as JSON", () => {
|
||||
const toolCalls = [
|
||||
createToolCall("1", "test", { data: { key: "value" } }),
|
||||
]
|
||||
|
||||
const xml = formatToolCallsAsXml(toolCalls)
|
||||
|
||||
expect(xml).toContain('{"key":"value"}')
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractThinking", () => {
|
||||
it("should extract thinking content", () => {
|
||||
const response = `<thinking>Let me analyze this.</thinking>
|
||||
Here is the answer.`
|
||||
|
||||
const result = extractThinking(response)
|
||||
|
||||
expect(result.thinking).toBe("Let me analyze this.")
|
||||
expect(result.content).toContain("Here is the answer.")
|
||||
expect(result.content).not.toContain("thinking")
|
||||
})
|
||||
|
||||
it("should handle multiple thinking blocks", () => {
|
||||
const response = `<thinking>First thought.</thinking>
|
||||
Some content.
|
||||
<thinking>Second thought.</thinking>
|
||||
More content.`
|
||||
|
||||
const result = extractThinking(response)
|
||||
|
||||
expect(result.thinking).toContain("First thought.")
|
||||
expect(result.thinking).toContain("Second thought.")
|
||||
})
|
||||
|
||||
it("should return original content if no thinking", () => {
|
||||
const response = "Just a regular response."
|
||||
|
||||
const result = extractThinking(response)
|
||||
|
||||
expect(result.thinking).toBe("")
|
||||
expect(result.content).toBe(response)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasToolCalls", () => {
|
||||
it("should return true if response has tool calls", () => {
|
||||
const response = `<tool_call name="get_lines"><path>a.ts</path></tool_call>`
|
||||
|
||||
expect(hasToolCalls(response)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false if response has no tool calls", () => {
|
||||
const response = "Just text without tool calls."
|
||||
|
||||
expect(hasToolCalls(response)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateToolCallParams", () => {
|
||||
it("should return valid for complete params", () => {
|
||||
const params = { path: "src/index.ts", start: 1, end: 10 }
|
||||
const required = ["path", "start", "end"]
|
||||
|
||||
const result = validateToolCallParams("get_lines", params, required)
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return errors for missing required params", () => {
|
||||
const params = { path: "src/index.ts" }
|
||||
const required = ["path", "start", "end"]
|
||||
|
||||
const result = validateToolCallParams("get_lines", params, required)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toHaveLength(2)
|
||||
expect(result.errors).toContain("Missing required parameter: start")
|
||||
expect(result.errors).toContain("Missing required parameter: end")
|
||||
})
|
||||
|
||||
it("should treat null and undefined as missing", () => {
|
||||
const params = { path: null, start: undefined }
|
||||
const required = ["path", "start"]
|
||||
|
||||
const result = validateToolCallParams("test", params, required)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should accept empty required array", () => {
|
||||
const params = {}
|
||||
const required: string[] = []
|
||||
|
||||
const result = validateToolCallParams("git_status", params, required)
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user