1813 lines
72 KiB
TypeScript
1813 lines
72 KiB
TypeScript
import { describe, it, expect, afterAll, beforeEach, afterEach } from 'vitest'
|
|
import { getClient, getZai, metadata } from './utils'
|
|
import { TableAdapter } from '../src/adapters/botpress-table'
|
|
import type { AnswerResult } from '../src/operations/answer'
|
|
import { parseResponse } from '../src/operations/answer'
|
|
|
|
describe('zai.answer', { timeout: 60_000 }, () => {
|
|
const zai = getZai()
|
|
|
|
describe('basic answer generation', () => {
|
|
it('should answer a simple question with citations (string documents)', async () => {
|
|
const documents = [
|
|
'Microsoft was founded in 1975 by Bill Gates and Paul Allen.',
|
|
'Botpress is an AI agent platform.\nIt was founded in 2016.\nThe company is based in Quebec, Canada.',
|
|
'Apple was founded by Steve Jobs, Steve Wozniak, and Ronald Wayne in 1976.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'When was Botpress founded?')
|
|
console.log(result)
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toContain('2016')
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
expect(result.citations[0].item).toBe(documents[1])
|
|
expect(result.citations[0].snippet).toMatchInlineSnapshot(`"It was founded in 2016."`)
|
|
}
|
|
})
|
|
|
|
it('should answer with object documents', async () => {
|
|
const documents = [
|
|
{ name: 'Botpress', type: 'AI Platform', founded: 2016 },
|
|
{ name: 'Dialogflow', type: 'Chatbot Platform', founded: 2011 },
|
|
{ name: 'Rasa', type: 'Chatbot Framework', founded: 2016 },
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What type of product is Botpress?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toContain('platform')
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
expect(result.citations[0].item).toBe(documents[0])
|
|
}
|
|
})
|
|
|
|
it('should provide full result with usage stats', async () => {
|
|
const documents = ['The sky is blue.', 'Grass is green.']
|
|
|
|
const { output, usage } = await zai.answer(documents, 'What color is the sky?').result()
|
|
|
|
expect(output.type).toBe('answer')
|
|
if (output.type === 'answer') {
|
|
expect(output.answer.toLowerCase()).toContain('blue')
|
|
}
|
|
expect(usage.requests.requests).toBeGreaterThanOrEqual(1)
|
|
expect(usage.requests.responses).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
it('should handle multiple citations in one answer', async () => {
|
|
const documents = [
|
|
'Botpress supports multiple languages including English, French, and Spanish.',
|
|
'The platform has built-in NLU capabilities.',
|
|
'Botpress can integrate with various LLM providers like OpenAI and Anthropic.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the main features of Botpress?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
// Should reference multiple documents
|
|
const uniqueItems = new Set(result.citations.map((c) => c.item))
|
|
expect(uniqueItems.size).toBeGreaterThan(1)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('citation formats', () => {
|
|
it('should handle single-line citations', async () => {
|
|
const documents = ['The capital of France is Paris.']
|
|
|
|
const result = await zai.answer(documents, 'What is the capital of France?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toContain('Paris')
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle range citations', async () => {
|
|
const documents = [
|
|
`France is a country in Western Europe.
|
|
It has a population of about 67 million people.
|
|
Paris is its capital and largest city.
|
|
France is known for its cuisine, wine, and culture.`,
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Tell me about France.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThan(2)
|
|
}
|
|
})
|
|
|
|
it('should handle non-contiguous citations', async () => {
|
|
const documents = [
|
|
'Botpress was founded in 2016.',
|
|
'It has offices in multiple countries.',
|
|
'The company is headquartered in Quebec, Canada.',
|
|
'It provides an AI agent platform.',
|
|
'The platform supports over 100 languages.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Where is Botpress located and when was it founded?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
expect(result.answer).toContain('2016')
|
|
expect(result.answer.toLowerCase()).toMatch(/quebec|canada/)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('ambiguous responses', () => {
|
|
it('should treat multiple ■answer tags as ambiguous (bad LLM generation)', () => {
|
|
// Simulate a bad LLM response with multiple ■answer tags
|
|
const malformedResponse = `■answer
|
|
This is the first answer.■001
|
|
■answer
|
|
This is the second answer.■002`
|
|
|
|
// Create mock line mappings
|
|
const documents = ['Document A: Information about topic A.', 'Document B: Information about topic B.']
|
|
const mappings = [
|
|
{
|
|
lineNumber: 1,
|
|
documentIndex: 0,
|
|
lineInDocument: 0,
|
|
text: 'Document A: Information about topic A.',
|
|
document: documents[0],
|
|
},
|
|
{
|
|
lineNumber: 2,
|
|
documentIndex: 1,
|
|
lineInDocument: 0,
|
|
text: 'Document B: Information about topic B.',
|
|
document: documents[1],
|
|
},
|
|
]
|
|
|
|
const result = parseResponse(malformedResponse, mappings)
|
|
|
|
// Should interpret multiple ■answer tags as ambiguous
|
|
expect(result.type).toBe('ambiguous')
|
|
if (result.type === 'ambiguous') {
|
|
expect(result.answers.length).toBe(2)
|
|
expect(result.answers[0].answer).toContain('first answer')
|
|
expect(result.answers[1].answer).toContain('second answer')
|
|
// Citations should be removed from answers
|
|
expect(result.answers[0].answer).not.toContain('■')
|
|
expect(result.answers[1].answer).not.toContain('■')
|
|
}
|
|
})
|
|
|
|
it('should detect ambiguity when question has multiple interpretations', async () => {
|
|
const documents = [
|
|
'Python is a programming language.',
|
|
'Python is also a type of snake.',
|
|
'The Python programming language was created by Guido van Rossum.',
|
|
'Python snakes are found in Africa, Asia, and Australia.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is Python?')
|
|
|
|
// Could be either answer or ambiguous
|
|
if (result.type === 'ambiguous') {
|
|
expect(result.ambiguity).toBeTruthy()
|
|
expect(result.follow_up).toBeTruthy()
|
|
expect(result.answers.length).toBeGreaterThanOrEqual(2)
|
|
expect(result.answers.length).toBeLessThanOrEqual(3)
|
|
expect(result.answers[0].answer).toBeTruthy()
|
|
expect(result.answers[0].citations.length).toBeGreaterThan(0)
|
|
} else if (result.type === 'answer') {
|
|
// If it picks one interpretation, that's also acceptable
|
|
expect(result.answer).toBeTruthy()
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
} else {
|
|
throw new Error('Expected answer or ambiguous response')
|
|
}
|
|
})
|
|
|
|
it('should provide multiple possible answers when ambiguous', async () => {
|
|
const documents = [
|
|
'The word "bank" can refer to a financial institution.',
|
|
'A bank can also mean the land alongside a river.',
|
|
'Banks play a crucial role in the economy.',
|
|
'River banks are important for ecosystems.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is a bank?')
|
|
|
|
if (result.type === 'ambiguous') {
|
|
expect(result.answers.length).toBeGreaterThanOrEqual(2)
|
|
result.answers.forEach((answer) => {
|
|
expect(answer.answer).toBeTruthy()
|
|
expect(answer.citations.length).toBeGreaterThan(0)
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('out of topic responses', () => {
|
|
it('should detect when question is completely unrelated to documents', async () => {
|
|
const documents = [
|
|
'Botpress is an AI platform.',
|
|
'It supports chatbot development.',
|
|
'The platform integrates with various LLMs.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is the recipe for chocolate cake?')
|
|
|
|
expect(result.type).toBe('out_of_topic')
|
|
if (result.type === 'out_of_topic') {
|
|
expect(result.reason).toBeTruthy()
|
|
expect(result.reason.length).toBeGreaterThan(10)
|
|
}
|
|
})
|
|
|
|
it('should detect out of topic even with partial keyword match', async () => {
|
|
const documents = [
|
|
'JavaScript is a programming language.',
|
|
'It runs in web browsers.',
|
|
'Node.js allows JavaScript to run on servers.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I cook java beans?')
|
|
|
|
expect(result.type).toBe('out_of_topic')
|
|
if (result.type === 'out_of_topic') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
|
|
it('should detect out of topic with highly technical documents and unrelated query', async () => {
|
|
const documents = [
|
|
'The Kubernetes control plane consists of the API server, scheduler, and controller manager.',
|
|
'Pods are the smallest deployable units in Kubernetes that can be created and managed.',
|
|
'A ReplicaSet ensures that a specified number of pod replicas are running at any given time.',
|
|
'Services in Kubernetes provide stable networking endpoints for pods.',
|
|
'ConfigMaps allow you to decouple configuration artifacts from container images.',
|
|
'Persistent Volumes (PV) provide storage resources in a cluster.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the health benefits of Mediterranean diet?')
|
|
|
|
expect(result.type).toBe('out_of_topic')
|
|
if (result.type === 'out_of_topic') {
|
|
expect(result.reason.toLowerCase()).toMatch(/kubernetes|technical|container|diet|health/)
|
|
}
|
|
})
|
|
|
|
it('should detect out of topic with medical documents and finance query', async () => {
|
|
const documents = [
|
|
'Myocardial infarction, commonly known as a heart attack, occurs when blood flow to the heart is blocked.',
|
|
'Symptoms include chest pain, shortness of breath, and pain in the arm or jaw.',
|
|
'Risk factors include high blood pressure, high cholesterol, smoking, and diabetes.',
|
|
'Treatment may involve medications such as aspirin, beta-blockers, and ACE inhibitors.',
|
|
'Coronary angioplasty and stenting are common interventional procedures.',
|
|
'Cardiac rehabilitation programs help patients recover and reduce future risk.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is the current federal reserve interest rate?')
|
|
|
|
expect(result.type).toBe('out_of_topic')
|
|
if (result.type === 'out_of_topic') {
|
|
expect(result.reason.toLowerCase()).toMatch(/medical|health|cardio|finance|interest|rate/)
|
|
}
|
|
})
|
|
|
|
it('should handle borderline case where topic tangentially related', async () => {
|
|
// Documents about programming in general, question about very specific unrelated language
|
|
const documents = [
|
|
'Python is a high-level programming language known for readability.',
|
|
'JavaScript is primarily used for web development.',
|
|
'Java is a statically-typed object-oriented language.',
|
|
'C++ offers low-level memory manipulation capabilities.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the key features of COBOL mainframe programming?')
|
|
|
|
// Could be either out_of_topic or missing_knowledge since it's about programming but very different
|
|
expect(['out_of_topic', 'missing_knowledge']).toContain(result.type)
|
|
if (result.type === 'out_of_topic') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('invalid question responses', () => {
|
|
it('should detect empty question', async () => {
|
|
const documents = ['Some information here.']
|
|
|
|
const result = await zai.answer(documents, '')
|
|
|
|
expect(result.type).toBe('invalid_question')
|
|
if (result.type === 'invalid_question') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
|
|
it('should detect non-question statements', async () => {
|
|
const documents = ['Botpress is an AI platform.']
|
|
|
|
const result = await zai.answer(documents, 'Botpress.')
|
|
|
|
// Could be invalid_question or could try to answer
|
|
if (result.type === 'invalid_question') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
|
|
it('should detect gibberish questions', async () => {
|
|
const documents = ['Real content here about technology.']
|
|
|
|
const result = await zai.answer(documents, 'asdf jkl; qwer?')
|
|
|
|
expect(['invalid_question', 'out_of_topic']).toContain(result.type)
|
|
if (result.type === 'invalid_question') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('missing knowledge responses', () => {
|
|
it('should detect when documents lack information to answer', async () => {
|
|
const documents = ['Botpress is a platform.', 'It was founded in Quebec.']
|
|
|
|
const result = await zai.answer(documents, 'What is the exact number of employees at Botpress?')
|
|
|
|
expect(['missing_knowledge', 'out_of_topic']).toContain(result.type)
|
|
if (result.type === 'missing_knowledge') {
|
|
expect(result.reason).toBeTruthy()
|
|
expect(result.reason.toLowerCase()).toMatch(/employee|information|knowledge/)
|
|
}
|
|
})
|
|
|
|
it('should detect when question requires specific data not in documents', async () => {
|
|
const documents = ['The company makes software.', 'It has customers worldwide.', 'The product is popular.']
|
|
|
|
const result = await zai.answer(documents, 'What was the revenue in Q3 2023?')
|
|
|
|
expect(['missing_knowledge', 'out_of_topic']).toContain(result.type)
|
|
if (result.type === 'missing_knowledge') {
|
|
expect(result.reason).toBeTruthy()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty documents array', async () => {
|
|
const result = await zai.answer([], 'What is Botpress?')
|
|
|
|
expect(['missing_knowledge', 'out_of_topic', 'invalid_question']).toContain(result.type)
|
|
})
|
|
|
|
it('should handle single document', async () => {
|
|
const documents = ['Botpress is an AI agent platform founded in 2016.']
|
|
|
|
const result = await zai.answer(documents, 'What is Botpress?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toContain('ai')
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle very long documents', async () => {
|
|
const longDoc =
|
|
'This is a very long document. ' +
|
|
'It contains many sentences. '.repeat(500) +
|
|
'The answer is 42. ' +
|
|
'More text here. '.repeat(500)
|
|
|
|
const documents = [longDoc]
|
|
|
|
const result = await zai.answer(documents, 'What is the answer?')
|
|
|
|
expect(['answer', 'missing_knowledge']).toContain(result.type)
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toContain('42')
|
|
}
|
|
})
|
|
|
|
it('should handle mixed document types', async () => {
|
|
const documents = ['Plain text document.', { type: 'object', value: 'Object document' }, ['array', 'document']]
|
|
|
|
const result = await zai.answer(documents, 'What types of documents are here?')
|
|
|
|
expect(['answer', 'ambiguous']).toContain(result.type)
|
|
})
|
|
|
|
it('should handle documents with special characters', async () => {
|
|
const documents = ['Email: support@botpress.com', 'Price: $99.99', 'Code: function() { return "hello"; }']
|
|
|
|
const result = await zai.answer(documents, 'What is the email address?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toContain('support@botpress.com')
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('large document sets - chunking and merging', () => {
|
|
it('should handle many documents that exceed single LLM call capacity', async () => {
|
|
// Create 100 documents, each with unique information
|
|
const documents = Array.from({ length: 100 }, (_, i) => {
|
|
return `Document ${i + 1}: This document contains information about topic ${i + 1}. The key fact is that value ${i + 1} is important for item ${i + 1}.`
|
|
})
|
|
|
|
// Add a few documents with the actual answer scattered throughout
|
|
documents[10] = 'The headquarters of Acme Corp is located in New York City.'
|
|
documents[50] = 'Acme Corp was founded in 1995 by John Smith.'
|
|
documents[90] = 'The company employs over 5000 people worldwide.'
|
|
|
|
const result = await zai.answer(documents, 'Tell me about Acme Corp.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should contain information from multiple chunks
|
|
expect(result.answer.toLowerCase()).toMatch(/new york|1995|5000/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Verify citations reference the correct documents
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems).toContain(documents[10])
|
|
expect(citedItems.some((item) => item === documents[50] || item === documents[90])).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should visit every document at least once when chunking', async () => {
|
|
// Create documents where the answer requires info from first and last
|
|
const documents = Array.from({ length: 50 }, (_, i) => `Filler document ${i + 1}.`)
|
|
|
|
documents[0] = 'The product name is AlphaBot.'
|
|
documents[49] = 'AlphaBot costs $299 per month.'
|
|
|
|
const result = await zai.answer(documents, 'What is the price of AlphaBot?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toContain('299')
|
|
// Should have citations from both the first and last document
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems).toContain(documents[49])
|
|
}
|
|
})
|
|
|
|
it('should merge answers from multiple chunks into unified answer', async () => {
|
|
// Create many documents with related information spread across them
|
|
const documents = [
|
|
...Array.from({ length: 30 }, (_, i) => `Irrelevant document ${i + 1}`),
|
|
'BetaProduct features: Real-time analytics, customizable dashboards.',
|
|
...Array.from({ length: 30 }, (_, i) => `More irrelevant content ${i + 1}`),
|
|
'BetaProduct pricing: Starts at $99/month for basic plan.',
|
|
...Array.from({ length: 30 }, (_, i) => `Even more filler ${i + 1}`),
|
|
'BetaProduct integrations: Works with Slack, Teams, and Discord.',
|
|
...Array.from({ length: 30 }, (_, i) => `Last batch of filler ${i + 1}`),
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the main features and pricing of BetaProduct?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should mention information from multiple chunks
|
|
expect(result.answer.toLowerCase()).toMatch(/analytics|dashboard/)
|
|
expect(result.answer).toContain('99')
|
|
|
|
// Should have citations from different parts of the document set
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle case where each chunk produces partial answer', async () => {
|
|
const documents = [
|
|
...Array.from({ length: 25 }, () => 'Filler content here.'),
|
|
'Step 1: Install the software using npm install.',
|
|
...Array.from({ length: 25 }, () => 'More filler content.'),
|
|
'Step 2: Configure the API key in your .env file.',
|
|
...Array.from({ length: 25 }, () => 'Additional filler.'),
|
|
'Step 3: Run the application with npm start.',
|
|
...Array.from({ length: 25 }, () => 'Final filler content.'),
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I set up the application?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should mention multiple steps from different chunks
|
|
const lowerAnswer = result.answer.toLowerCase()
|
|
expect(lowerAnswer).toMatch(/install|configure|run/)
|
|
expect(result.citations.length).toBeGreaterThan(1)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('citation markers removed from answers', () => {
|
|
it('should remove all citation markers (■) from answer text', async () => {
|
|
const documents = [
|
|
'Botpress was founded in 2016.',
|
|
'It is an AI agent platform.',
|
|
'The company is headquartered in Quebec, Canada.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Tell me about Botpress.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should have citations
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
// Answer should NOT contain any citation markers (■)
|
|
expect(result.answer).not.toContain('■')
|
|
}
|
|
})
|
|
|
|
it('should remove all citation markers (■) from ambiguous answers', async () => {
|
|
const documents = [
|
|
'Python is a programming language.',
|
|
'Python is also a type of snake.',
|
|
'The Python programming language was created by Guido van Rossum.',
|
|
'Python snakes are found in Africa, Asia, and Australia.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is Python?')
|
|
|
|
if (result.type === 'ambiguous') {
|
|
// Check ambiguity and follow_up don't contain markers
|
|
expect(result.ambiguity).not.toContain('■')
|
|
expect(result.follow_up).not.toContain('■')
|
|
// Check each answer doesn't contain markers
|
|
result.answers.forEach((answer) => {
|
|
expect(answer.answer).not.toContain('■')
|
|
})
|
|
}
|
|
})
|
|
|
|
it('should handle multiple citations throughout answer without leaving markers', async () => {
|
|
const documents = [
|
|
'The iPhone was first released by Apple in 2007.',
|
|
'Steve Jobs announced the iPhone at the Macworld conference.',
|
|
'The original iPhone had a 3.5-inch display and 2-megapixel camera.',
|
|
'The iPhone revolutionized the smartphone industry.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'When was the iPhone released and who announced it?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should have multiple citations
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
// But answer should be completely clean of markers
|
|
expect(result.answer).not.toContain('■')
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('citation offsets', () => {
|
|
it('should have valid citation offsets in answer text', async () => {
|
|
const documents = ['The capital of France is Paris.', 'Paris has a population of 2.2 million.']
|
|
|
|
const result = await zai.answer(documents, 'What is the capital of France?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
result.citations.forEach((citation) => {
|
|
expect(citation.offset).toBeGreaterThanOrEqual(0)
|
|
expect(citation.offset).toBeLessThanOrEqual(result.answer.length + 1)
|
|
})
|
|
}
|
|
})
|
|
|
|
it('should have citations sorted by offset', async () => {
|
|
const documents = [
|
|
'Botpress is based in Quebec.',
|
|
'It was founded in 2016.',
|
|
'The platform supports many languages.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Tell me about Botpress.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer' && result.citations.length > 1) {
|
|
for (let i = 1; i < result.citations.length; i++) {
|
|
expect(result.citations[i].offset).toBeGreaterThanOrEqual(result.citations[i - 1].offset)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('multiple citations throughout answer', () => {
|
|
it('should cite different sources for different parts of the answer', async () => {
|
|
const documents = [
|
|
'The iPhone was first released by Apple in 2007.',
|
|
'Steve Jobs announced the iPhone at the Macworld conference.',
|
|
'The original iPhone had a 3.5-inch display and 2-megapixel camera.',
|
|
'The iPhone revolutionized the smartphone industry.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'When was the iPhone released and who announced it?', {
|
|
instructions: 'Answer in two different sentences. Cite each sentences.',
|
|
})
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
console.log(result.citations)
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Should cite the release year from one document
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems).toContain(documents[0]) // For 2007
|
|
expect(citedItems).toContain(documents[1]) // For Steve Jobs
|
|
|
|
// Citations should be at different offsets (different parts of answer)
|
|
const uniqueOffsets = new Set(result.citations.map((c) => c.offset))
|
|
expect(uniqueOffsets.size).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should have citations distributed throughout a longer answer', async () => {
|
|
const documents = [
|
|
'TypeScript is a statically typed superset of JavaScript.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'TypeScript was developed by Microsoft and first released in 2012.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'TypeScript code compiles to plain JavaScript.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'Typescript: Major features include type annotations, interfaces, and generics.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'TypeScript is widely used in Angular, Vue 3, and other frameworks.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'Sky is blue due to the scattering of sunlight by the atmosphere.',
|
|
'--------------------------',
|
|
'The TypeScript compiler is called tsc.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is TypeScript, who created it, and what are its main features?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(3)
|
|
|
|
// Should have citations from multiple documents
|
|
const uniqueDocs = new Set(result.citations.map((c) => c.item))
|
|
expect(uniqueDocs.size).toBeGreaterThanOrEqual(3)
|
|
|
|
// Citations should be spread throughout the answer (not clustered)
|
|
const offsets = result.citations.map((c) => c.offset).sort((a, b) => a - b)
|
|
if (offsets.length >= 3) {
|
|
const answerLength = result.answer.length
|
|
// First citation should be in first half
|
|
expect(offsets[0]).toBeLessThan(answerLength / 2)
|
|
// Last citation should be in second half
|
|
expect(offsets[offsets.length - 1]).toBeGreaterThan(answerLength / 4)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should cite specific facts from different documents in sequence', async () => {
|
|
const documents = [
|
|
'The Great Wall of China stretches over 13,000 miles.',
|
|
'Construction began in the 7th century BC.',
|
|
'The wall was built to protect against invasions from the north.',
|
|
'It is made primarily of stone, brick, and wood.',
|
|
'The Great Wall is a UNESCO World Heritage Site.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is the Great Wall of China and why was it built?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
|
|
// Should mention length and cite that document
|
|
if (result.answer.includes('13,000') || result.answer.includes('miles')) {
|
|
expect(citedItems).toContain(documents[0])
|
|
}
|
|
|
|
// Should mention purpose and cite that document
|
|
if (result.answer.toLowerCase().includes('protect') || result.answer.toLowerCase().includes('invasion')) {
|
|
expect(citedItems).toContain(documents[2])
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should handle answer with many citations from many sources', async () => {
|
|
const documents = [
|
|
'React was created by Jordan Walke at Facebook.',
|
|
"React was first deployed on Facebook's newsfeed in 2011.",
|
|
'React Native was released in 2015 for mobile development.',
|
|
'React uses a virtual DOM for efficient rendering.',
|
|
'JSX is the syntax extension used in React.',
|
|
'Hooks were introduced in React 16.8.',
|
|
'React is maintained by Meta and the community.',
|
|
'Popular React frameworks include Next.js and Gatsby.',
|
|
]
|
|
|
|
const result = await zai.answer(
|
|
documents,
|
|
'What is the history of React and what are its key technical features?'
|
|
)
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should have multiple citations
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(4)
|
|
|
|
// Should reference at least 4 different documents
|
|
const uniqueDocs = new Set(result.citations.map((c) => c.item))
|
|
expect(uniqueDocs.size).toBeGreaterThanOrEqual(4)
|
|
|
|
// All offsets should be unique or at least varied
|
|
const offsets = result.citations.map((c) => c.offset)
|
|
const uniqueOffsets = new Set(offsets)
|
|
expect(uniqueOffsets.size).toBeGreaterThanOrEqual(3)
|
|
}
|
|
})
|
|
|
|
it('should cite different documents for contrasting information', async () => {
|
|
const documents = [
|
|
'Traditional databases use a centralized architecture with a single source of truth.',
|
|
'Distributed databases spread data across multiple nodes for redundancy.',
|
|
'SQL databases use structured schemas and relationships.',
|
|
'NoSQL databases offer flexible schemas and horizontal scaling.',
|
|
'ACID properties ensure data consistency in traditional databases.',
|
|
'BASE properties prioritize availability in distributed systems.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the differences between SQL and NoSQL databases?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
|
|
// Should cite both SQL and NoSQL related documents
|
|
const hasSQLCitation = citedItems.some(
|
|
(item) => typeof item === 'string' && (item.includes('SQL') || item.includes('ACID'))
|
|
)
|
|
const hasNoSQLCitation = citedItems.some(
|
|
(item) => typeof item === 'string' && (item.includes('NoSQL') || item.includes('BASE'))
|
|
)
|
|
|
|
expect(hasSQLCitation).toBe(true)
|
|
expect(hasNoSQLCitation).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('complex formatting in answers', () => {
|
|
it('should format code blocks in the answer', async () => {
|
|
const documents = [
|
|
'To create a React component, use the following syntax:\nfunction MyComponent() {\n return <div>Hello</div>;\n}',
|
|
'React components must return JSX elements.',
|
|
'You can export the component using: export default MyComponent;',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I create a React component?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should contain code-like formatting
|
|
expect(result.answer).toMatch(/function|return|export/)
|
|
// Should have citations
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle multi-line code examples with citations', async () => {
|
|
const documents = [
|
|
`Here's how to define a Python class:
|
|
class Person:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def greet(self):
|
|
return f"Hello, {self.name}"`,
|
|
'Python uses indentation to define code blocks.',
|
|
'The __init__ method is the constructor in Python classes.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I create a Python class?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should preserve code structure
|
|
expect(result.answer).toMatch(/class|def|self/)
|
|
// Should cite the code example
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems.some((item) => typeof item === 'string' && item.includes('class Person'))).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should format SQL queries in answers', async () => {
|
|
const documents = [
|
|
'To select all users: SELECT * FROM users;',
|
|
'To filter by age: SELECT * FROM users WHERE age > 18;',
|
|
'Use JOIN to combine tables: SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id;',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I query all users from the database?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/SELECT.*FROM.*users/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle JSON examples in answers', async () => {
|
|
const documents = [
|
|
'API configuration format:\n{\n "apiKey": "your-key",\n "endpoint": "https://api.example.com",\n "timeout": 5000\n}',
|
|
'The apiKey field is required for authentication.',
|
|
'Timeout is specified in milliseconds.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is the API configuration format?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should contain JSON-like structure
|
|
expect(result.answer).toMatch(/apiKey|endpoint|timeout/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle shell commands with citations', async () => {
|
|
const documents = [
|
|
'Install the package: npm install react',
|
|
'Start the development server: npm start',
|
|
'Build for production: npm run build',
|
|
'Run tests: npm test',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What commands do I need to set up and run the project?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/npm/)
|
|
// Should cite multiple command examples
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should format mixed content with code and explanations', async () => {
|
|
const documents = [
|
|
'The useEffect hook runs side effects. Usage: useEffect(() => { /* effect */ }, [dependencies])',
|
|
'The dependency array controls when the effect re-runs.',
|
|
'Empty array [] means the effect runs once on mount.',
|
|
'No array means the effect runs after every render.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How does the useEffect hook work in React?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should contain both explanation and code
|
|
expect(result.answer.toLowerCase()).toMatch(/effect|run/)
|
|
expect(result.answer).toMatch(/useEffect|\[\]/)
|
|
// Should cite both code example and explanations
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should handle markdown-style formatting', async () => {
|
|
const documents = [
|
|
'Authentication steps:\n1. Send credentials to /auth/login\n2. Receive JWT token\n3. Include token in Authorization header',
|
|
'The token expires after 24 hours.',
|
|
'Format: Authorization: Bearer <token>',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I authenticate with the API?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should preserve numbered steps or similar structure
|
|
expect(result.answer).toMatch(/token|auth|login/i)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should format configuration files with proper structure', async () => {
|
|
const documents = [
|
|
`Docker Compose configuration:
|
|
version: '3.8'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
ports:
|
|
- "80:80"
|
|
db:
|
|
image: postgres:13
|
|
environment:
|
|
POSTGRES_PASSWORD: secret`,
|
|
'The version specifies the Docker Compose file format.',
|
|
'Services define the containers to run.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Show me a Docker Compose configuration example.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/version|services|image/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
// Should cite the configuration example
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems.some((item) => typeof item === 'string' && item.includes('version'))).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should handle inline code snippets within prose', async () => {
|
|
const documents = [
|
|
'Use the `useState` hook to add state to functional components.',
|
|
'Call it like this: const [count, setCount] = useState(0)',
|
|
'The first value is the state, the second is the setter function.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I use useState in React?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/useState/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should format complex algorithm examples', async () => {
|
|
const documents = [
|
|
`Binary search implementation:
|
|
function binarySearch(arr, target) {
|
|
let left = 0;
|
|
let right = arr.length - 1;
|
|
|
|
while (left <= right) {
|
|
const mid = Math.floor((left + right) / 2);
|
|
if (arr[mid] === target) return mid;
|
|
if (arr[mid] < target) left = mid + 1;
|
|
else right = mid - 1;
|
|
}
|
|
return -1;
|
|
}`,
|
|
'Binary search has O(log n) time complexity.',
|
|
'The array must be sorted for binary search to work.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How does binary search work?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should include the algorithm
|
|
expect(result.answer).toMatch(/binary|search|left|right/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle regex patterns in answers', async () => {
|
|
const documents = [
|
|
'Email validation regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/',
|
|
'Phone number pattern: /^\\+?1?\\d{10}$/',
|
|
'Use RegExp test() method to validate: pattern.test(input)',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I validate an email address?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/email|regex|pattern/i)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should format CSS code examples', async () => {
|
|
const documents = [
|
|
`.container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
}`,
|
|
'Flexbox is used for flexible layouts.',
|
|
'justify-content centers items horizontally.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I center content with flexbox?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/flex|center|justify-content/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should preserve code formatting with multiple languages', async () => {
|
|
const documents = [
|
|
'JavaScript: async function fetchData() { const res = await fetch(url); return res.json(); }',
|
|
'Python: async def fetch_data(): response = await http.get(url); return response.json()',
|
|
'Both languages support async/await syntax for asynchronous operations.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'How do I fetch data asynchronously?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/async|await|fetch/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle API response examples with formatting', async () => {
|
|
const documents = [
|
|
`Success response:
|
|
{
|
|
"status": 200,
|
|
"data": {
|
|
"id": 123,
|
|
"name": "John Doe"
|
|
},
|
|
"message": "User retrieved successfully"
|
|
}`,
|
|
`Error response:
|
|
{
|
|
"status": 404,
|
|
"error": "User not found",
|
|
"code": "USER_NOT_FOUND"
|
|
}`,
|
|
'Status codes indicate success (2xx) or errors (4xx, 5xx).',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What do API responses look like?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/status|response|data/)
|
|
// Should cite both success and error examples
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should maintain code structure integrity with citations', async () => {
|
|
const documents = [
|
|
`Step 1 - Import dependencies:
|
|
import React from 'react';
|
|
import { useState } from 'react';`,
|
|
`Step 2 - Create the component:
|
|
function Counter() {
|
|
const [count, setCount] = useState(0);
|
|
return <div>{count}</div>;
|
|
}`,
|
|
`Step 3 - Export:
|
|
export default Counter;`,
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Show me how to create a counter component.')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should include steps with code
|
|
expect(result.answer).toMatch(/import|function|export/)
|
|
// Should cite multiple steps
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Citations should be ordered through the answer
|
|
const offsets = result.citations.map((c) => c.offset)
|
|
for (let i = 1; i < offsets.length; i++) {
|
|
expect(offsets[i]).toBeGreaterThanOrEqual(offsets[i - 1])
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('finding relevant documents in large noise', () => {
|
|
// Seeded random number generator for deterministic shuffling
|
|
const seededRandom = (seed: number) => {
|
|
let state = seed
|
|
return () => {
|
|
state = (state * 1103515245 + 12345) & 0x7fffffff
|
|
return state / 0x7fffffff
|
|
}
|
|
}
|
|
|
|
const shuffle = <T>(array: T[], seed: number): T[] => {
|
|
const arr = [...array]
|
|
const random = seededRandom(seed)
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(random() * (i + 1))
|
|
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
|
}
|
|
return arr
|
|
}
|
|
|
|
it('should find relevant docs among 1000 unrelated documents', async () => {
|
|
const relevantDocs = [
|
|
`HOW QUANTUM ENCRYPTION WORKS: Quantum encryption uses quantum key distribution (QKD) to securely share encryption keys.\nQKD leverages the principles of quantum mechanics to detect eavesdropping.\nThe BB84 protocol is the first and most well-known QKD protocol.\n'Quantum bits (qubits) can exist in superposition, allowing for secure key exchange.`,
|
|
]
|
|
|
|
// Create 1000 noise documents
|
|
const noiseDocs = Array.from({ length: 1000 }, (_, i) => {
|
|
const topics = [
|
|
`Recipe ${i}: Mix flour, water, and yeast to make bread dough.`,
|
|
`Historical event ${i}: In the year ${1800 + i}, various political changes occurred.`,
|
|
`Sports fact ${i}: The championship was won by team Alpha in overtime.`,
|
|
`Movie review ${i}: This film features excellent cinematography and compelling characters.`,
|
|
`Travel guide ${i}: The beach resort offers stunning views and great amenities.`,
|
|
]
|
|
return topics[i % topics.length]
|
|
})
|
|
|
|
// Shuffle relevant docs into the noise (deterministic seed: 42)
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 42)
|
|
|
|
const result = await zai.answer(allDocs, 'How does quantum encryption work?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/quantum|qkd|encryption/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Should cite relevant docs, not noise
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
const citedRelevant = citedItems.filter((item) => relevantDocs.some((relevant) => item === relevant)).length
|
|
const citedNoise = citedItems.filter((item) => noiseDocs.some((noise) => item === noise)).length
|
|
|
|
expect(citedRelevant).toBeGreaterThan(0)
|
|
// Should primarily cite relevant docs (allow some noise due to LLM variability)
|
|
expect(citedRelevant).toBeGreaterThan(citedNoise)
|
|
}
|
|
})
|
|
|
|
it('should find scattered relevant info in 500 noise documents', async () => {
|
|
const relevantDocs = [
|
|
'Neural Networks: How they learn. Neural networks are inspired by the human brain. They learn patterns from data.',
|
|
'Neural Networks: How they learn. Neural networks consist of interconnected nodes organized in layers.',
|
|
'Neural Networks: How they learn. Backpropagation is the algorithm used to train neural networks.',
|
|
'Neural Networks: How they learn. Activation functions introduce non-linearity into the network.',
|
|
'Neural Networks: How they learn. Deep learning uses neural networks with many hidden layers.',
|
|
'Neural Networks: How they learn. Gradient descent optimizes the network weights during training.',
|
|
]
|
|
|
|
const noiseDocs = Array.from({ length: 500 }, (_, i) => {
|
|
const topics = [
|
|
`Gardening tip ${i}: Water plants early in the morning for best results.`,
|
|
`Fashion trend ${i}: This season features bold colors and unique patterns.`,
|
|
`Car maintenance ${i}: Regular oil changes extend engine life significantly.`,
|
|
`Weather report ${i}: Expect partly cloudy skies with mild temperatures.`,
|
|
]
|
|
return topics[i % topics.length]
|
|
})
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 123)
|
|
|
|
const result = await zai.answer(allDocs, 'How do neural networks learn?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/neural|network|backpropagation|gradient/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
const hasRelevantCitations = citedItems.some((item) => relevantDocs.some((relevant) => item === relevant))
|
|
expect(hasRelevantCitations).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should handle relevant docs at beginning buried in 800 noise docs', async () => {
|
|
const relevantDocs = [
|
|
'The mitochondria is the powerhouse of the cell.',
|
|
'It produces ATP through cellular respiration.',
|
|
'Mitochondria have their own DNA separate from nuclear DNA.',
|
|
]
|
|
|
|
const noiseDocs = Array.from({ length: 800 }, (_, i) => `Unrelated content ${i} about random topics.`)
|
|
|
|
// Place relevant docs at the start, then shuffle
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 456)
|
|
|
|
const result = await zai.answer(allDocs, 'What is the function of mitochondria?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/mitochondria|atp|energy|powerhouse/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
const citedRelevant = result.citations.filter((c) => relevantDocs.includes(c.item as string)).length
|
|
expect(citedRelevant).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should find single relevant doc among 1200 noise documents', async () => {
|
|
const relevantDoc = 'The speed of light in a vacuum is approximately 299,792,458 meters per second.'
|
|
|
|
const noiseDocs = Array.from({ length: 1200 }, (_, i) => {
|
|
const topics = [
|
|
`Document ${i}: Information about unrelated scientific topics.`,
|
|
`Article ${i}: Discussion of various historical events and figures.`,
|
|
`Report ${i}: Analysis of economic trends and market conditions.`,
|
|
]
|
|
return topics[i % topics.length]
|
|
})
|
|
|
|
const allDocs = shuffle([relevantDoc, ...noiseDocs], 789)
|
|
|
|
const result = await zai.answer(allDocs, 'What is the speed of light?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/299,792,458|speed.*light/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
expect(result.citations.some((c) => c.item === relevantDoc)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should extract multi-part answer from docs scattered in 600 noise docs', async () => {
|
|
const relevantDocs = [
|
|
'HTTP status code 200 means OK - the request succeeded.',
|
|
'Status code 404 means Not Found - the requested resource does not exist.',
|
|
'Status code 500 means Internal Server Error - the server encountered an error.',
|
|
'Status code 401 means Unauthorized - authentication is required.',
|
|
]
|
|
|
|
const noiseDocs = Array.from(
|
|
{ length: 600 },
|
|
(_, i) => `Random document ${i} containing information about topics like cooking, sports, and music.`
|
|
)
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 321)
|
|
|
|
const result = await zai.answer(allDocs, 'What do different HTTP status codes mean?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should mention multiple status codes
|
|
const answer = result.answer.toLowerCase()
|
|
const mentionsMultiple =
|
|
[/200|ok/, /404|not found/, /500|error/, /401|unauthorized/].filter((pattern) => pattern.test(answer))
|
|
.length >= 2
|
|
|
|
expect(mentionsMultiple).toBe(true)
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Should cite relevant docs
|
|
const citedRelevant = result.citations.filter((c) => relevantDocs.includes(c.item as string)).length
|
|
expect(citedRelevant).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should prioritize relevant docs over keyword-matching noise', async () => {
|
|
const relevantDocs = [
|
|
'Rust is a systems programming language focused on safety and concurrency.',
|
|
'Rust prevents memory safety bugs through its ownership system.',
|
|
'The Rust compiler enforces memory safety at compile time.',
|
|
]
|
|
|
|
// Noise docs that mention "rust" but in different context
|
|
const confusingNoiseDocs = Array.from({ length: 300 }, (_, i) => {
|
|
const misleading = [
|
|
`Rust on metal ${i}: Iron oxide forms when metal is exposed to moisture.`,
|
|
`Preventing rust ${i}: Apply protective coating to metal surfaces regularly.`,
|
|
`Rust removal ${i}: Use vinegar or commercial rust removers for best results.`,
|
|
]
|
|
return misleading[i % misleading.length]
|
|
})
|
|
|
|
const otherNoiseDocs = Array.from({ length: 400 }, (_, i) => `General content ${i} about various topics.`)
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...confusingNoiseDocs, ...otherNoiseDocs], 654)
|
|
|
|
const result = await zai.answer(allDocs, 'What is Rust programming language?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/programming|language|memory|safety/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Should cite programming Rust, not metal rust
|
|
const citedItems = result.citations.map((c) => c.item as string)
|
|
const citedProgramming = citedItems.filter((item) => relevantDocs.includes(item)).length
|
|
const citedMetalRust = citedItems.filter((item) => confusingNoiseDocs.some((noise) => item === noise)).length
|
|
|
|
expect(citedProgramming).toBeGreaterThan(0)
|
|
// Should strongly prefer programming context over metal rust
|
|
if (citedMetalRust > 0) {
|
|
expect(citedProgramming).toBeGreaterThan(citedMetalRust)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should handle technical docs buried in 1500 generic documents', async () => {
|
|
const relevantDocs = [
|
|
'The CAP theorem states that distributed systems can only guarantee two of three properties: Consistency, Availability, and Partition tolerance.',
|
|
'In practice, partition tolerance is mandatory, so the choice is between consistency and availability.',
|
|
'CP systems prioritize consistency over availability during network partitions.',
|
|
'AP systems prioritize availability over consistency during network partitions.',
|
|
]
|
|
|
|
const noiseDocs = Array.from({ length: 1500 }, (_, i) => {
|
|
const generic = [
|
|
`Generic statement ${i}: Various things happen in different contexts.`,
|
|
`Common knowledge ${i}: Many people believe different things about various topics.`,
|
|
`General observation ${i}: Situations can vary depending on circumstances.`,
|
|
]
|
|
return generic[i % generic.length]
|
|
})
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 987)
|
|
|
|
const result = await zai.answer(allDocs, 'What is the CAP theorem in distributed systems?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/cap|consistency|availability|partition/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
const citedRelevant = result.citations.filter((c) => relevantDocs.includes(c.item as string)).length
|
|
expect(citedRelevant).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should find code examples buried in 700 non-code documents', async () => {
|
|
const relevantDocs = [
|
|
'Fibonacci in Python:\ndef fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)',
|
|
'The Fibonacci sequence starts with 0 and 1.',
|
|
'Each subsequent number is the sum of the previous two.',
|
|
]
|
|
|
|
const noiseDocs = Array.from(
|
|
{ length: 700 },
|
|
(_, i) => `Text document ${i} with prose about history, culture, and general knowledge without code.`
|
|
)
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 111)
|
|
|
|
const result = await zai.answer(allDocs, 'How do I implement Fibonacci in Python?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/def|fibonacci|fib/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Should cite the code example
|
|
const citedItems = result.citations.map((c) => c.item as string)
|
|
expect(citedItems.some((item) => item.includes('def fib'))).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should ignore noise and return missing_knowledge if no relevant docs exist', async () => {
|
|
const noiseDocs = Array.from({ length: 1000 }, (_, i) => {
|
|
const topics = [
|
|
`Cooking recipe ${i}: Combine ingredients and bake for 30 minutes.`,
|
|
`Travel destination ${i}: Beautiful scenery and cultural attractions.`,
|
|
`Movie synopsis ${i}: A thrilling story with unexpected plot twists.`,
|
|
]
|
|
return topics[i % topics.length]
|
|
})
|
|
|
|
const allDocs = shuffle(noiseDocs, 222)
|
|
|
|
const result = await zai.answer(allDocs, 'Explain quantum entanglement in detail.')
|
|
|
|
expect(['missing_knowledge', 'out_of_topic']).toContain(result.type)
|
|
})
|
|
|
|
it('should efficiently process 2000 docs and find 3 relevant ones', async () => {
|
|
const relevantDocs = [
|
|
'GDPR is the General Data Protection Regulation enacted by the European Union.',
|
|
'GDPR gives individuals control over their personal data.',
|
|
'Companies must obtain explicit consent to process personal data under GDPR.',
|
|
]
|
|
|
|
const noiseDocs = Array.from({ length: 2000 }, (_, i) => `Document ${i}: General business content.`)
|
|
|
|
const allDocs = shuffle([...relevantDocs, ...noiseDocs], 333)
|
|
|
|
const { output, usage } = await zai.answer(allDocs, 'What is GDPR?').result()
|
|
|
|
expect(output.type).toBe('answer')
|
|
if (output.type === 'answer') {
|
|
expect(output.answer.toLowerCase()).toMatch(/gdpr|general data protection/)
|
|
expect(output.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('documents of any type', () => {
|
|
it('should handle object documents with nested properties', async () => {
|
|
const documents = [
|
|
{
|
|
id: 1,
|
|
title: 'Introduction to GraphQL',
|
|
content: 'GraphQL is a query language for APIs developed by Facebook.',
|
|
metadata: { author: 'John Doe', year: 2015 },
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'GraphQL vs REST',
|
|
content: 'GraphQL allows clients to request exactly the data they need.',
|
|
metadata: { author: 'Jane Smith', year: 2018 },
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'GraphQL Schema',
|
|
content: 'The schema defines the types and relationships in your GraphQL API.',
|
|
metadata: { author: 'Bob Johnson', year: 2019 },
|
|
},
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is GraphQL?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/graphql|query language|api/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Citations should reference the original objects
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
expect(citedItems.some((item) => typeof item === 'object' && item !== null)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should cite correct object when multiple objects have similar content', async () => {
|
|
const documents = [
|
|
{ id: 'doc1', type: 'article', title: 'AI Ethics', content: 'Discussion about AI ethics and safety.' },
|
|
{ id: 'doc2', type: 'article', title: 'AI Development', content: 'How to develop AI systems.' },
|
|
{ id: 'doc3', type: 'article', title: 'AI Safety', content: 'Ensuring AI systems are safe and aligned.' },
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What does the document say about AI safety?')
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/safety|aligned|safe/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Should cite the AI Safety document specifically
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
const citedSafetyDoc = citedItems.some(
|
|
(item) => typeof item === 'object' && item !== null && 'id' in item && item.id === 'doc3'
|
|
)
|
|
expect(citedSafetyDoc).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('chunkLength option (token limits)', () => {
|
|
it('should respect custom chunkLength for document processing', async () => {
|
|
const documents = Array.from({ length: 50 }, (_, i) => `Document ${i}: This is content about topic ${i}.`)
|
|
|
|
// Force small chunks with chunkLength option
|
|
const { output, usage } = await zai.answer(documents, 'What topics are covered?', { chunkLength: 2000 }).result()
|
|
|
|
expect(output.type).toBe('answer')
|
|
if (output.type === 'answer') {
|
|
expect(output.answer).toBeTruthy()
|
|
expect(output.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle very large chunkLength (maximum: 100000 tokens)', async () => {
|
|
const largeDocs = Array.from(
|
|
{ length: 20 },
|
|
(_, i) =>
|
|
`Section ${i}: ` +
|
|
'This is a very long document section with lots of content. '.repeat(100) +
|
|
`Key point ${i}: Important information here.`
|
|
)
|
|
|
|
const result = await zai.answer(largeDocs, 'What are the key points?', { chunkLength: 100_000 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toBeTruthy()
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should trigger chunking when documents exceed chunkLength', async () => {
|
|
// Create documents that will definitely need chunking
|
|
const longDocs = Array.from({ length: 100 }, (_, i) => {
|
|
if (i === 42) {
|
|
return 'THE PASSWORD IS 68742. Remember this important fact.'
|
|
}
|
|
const content = `Document ${i}: `.repeat(10) + 'Content '.repeat(500)
|
|
return content
|
|
})
|
|
|
|
const { output, usage } = await zai.answer(longDocs, 'What is the password?', { chunkLength: 1000 }).result()
|
|
|
|
expect(output.type).toBe('answer')
|
|
if (output.type === 'answer') {
|
|
expect(output.answer).toMatch(/68742/)
|
|
}
|
|
})
|
|
|
|
it('should process all documents even with aggressive chunking', async () => {
|
|
const documents = [
|
|
'Important fact: quantum mechanics explains the behavior of particles at atomic scales.',
|
|
...Array.from({ length: 200 }, (_, i) => `Filler document ${i} with unrelated content.`),
|
|
'Important fact: relativity theory describes the gravitational interaction in the universe.',
|
|
...Array.from({ length: 200 }, (_, i) => `More filler document ${i + 200}.`),
|
|
'Important fact: thermodynamics describes the behavior of energy and matter.',
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What are the important facts?', { chunkLength: 500 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
// Should find at least 2 of the 3 important facts
|
|
const answer = result.answer.toLowerCase()
|
|
const foundFacts = [/quantum/, /relativity/, /thermodynamics/].filter((pattern) =>
|
|
pattern.test(answer.toLowerCase())
|
|
).length
|
|
|
|
expect(foundFacts).toBeGreaterThanOrEqual(3)
|
|
}
|
|
})
|
|
|
|
it('should merge results correctly with medium chunkLength', async () => {
|
|
const documents = [
|
|
...Array.from({ length: 30 }, (_, i) => `Noise ${i}`),
|
|
'React was created by Facebook.',
|
|
...Array.from({ length: 30 }, (_, i) => `Noise ${i + 30}`),
|
|
'React uses a virtual DOM.',
|
|
...Array.from({ length: 30 }, (_, i) => `Noise ${i + 60}`),
|
|
'React supports component composition.',
|
|
...Array.from({ length: 30 }, (_, i) => `Noise ${i + 90}`),
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'What is React?', { chunkLength: 3000 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/react/)
|
|
// Should cite multiple React-related documents from different chunks
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
}
|
|
})
|
|
|
|
it('should handle chunkLength with complex object documents', async () => {
|
|
const documents = Array.from({ length: 80 }, (_, i) => ({
|
|
id: i,
|
|
type: 'entry',
|
|
content: `Entry ${i} contains generic information about topic ${i}.`,
|
|
metadata: { created: '2025-03-12', tags: ['general'] },
|
|
}))
|
|
|
|
// Add relevant docs scattered throughout
|
|
documents[20] = {
|
|
id: 20,
|
|
type: 'important',
|
|
content: 'Kubernetes is a container orchestration platform.',
|
|
metadata: { created: '2025-03-12', tags: ['kubernetes', 'devops'] },
|
|
}
|
|
documents[50] = {
|
|
id: 50,
|
|
type: 'important',
|
|
content: 'Kubernetes automates deployment, scaling, and management of containerized applications.',
|
|
metadata: { created: '2025-03-12', tags: ['kubernetes', 'containers'] },
|
|
}
|
|
|
|
const result = await zai.answer(documents, 'What is Kubernetes?', { chunkLength: 2500 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/kubernetes|container|orchestration/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
|
|
// Should cite the relevant object documents
|
|
const citedItems = result.citations.map((c) => c.item)
|
|
const citedRelevant = citedItems.filter(
|
|
(item) => typeof item === 'object' && item !== null && 'type' in item && item.type === 'important'
|
|
).length
|
|
expect(citedRelevant).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle chunkLength smaller than a single large document', async () => {
|
|
const largeDocument =
|
|
'Introduction: '.repeat(100) +
|
|
'The theory of relativity was developed by Albert Einstein in the early 20th century. ' +
|
|
'It consists of special relativity and general relativity. '.repeat(50) +
|
|
'Special relativity deals with objects moving at constant velocity. ' +
|
|
'General relativity extends this to include acceleration and gravity. '.repeat(50)
|
|
|
|
const documents = [largeDocument, 'Additional context about Einstein.', 'More information about physics.']
|
|
|
|
// Chunk length smaller than the large document
|
|
const result = await zai.answer(documents, 'Who developed the theory of relativity?', { chunkLength: 1000 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/Einstein/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should validate chunkLength bounds (min: 100, max: 100000)', async () => {
|
|
const documents = ['Test document.']
|
|
|
|
// Should accept minimum value
|
|
await expect(zai.answer(documents, 'Test question?', { chunkLength: 1001 })).resolves.toBeTruthy()
|
|
|
|
// Should accept maximum value
|
|
await expect(zai.answer(documents, 'Test question?', { chunkLength: 100_000 })).resolves.toBeTruthy()
|
|
|
|
// Should reject below minimum
|
|
await expect(zai.answer(documents, 'Test question?', { chunkLength: 50 })).rejects.toThrow()
|
|
|
|
// Should reject above maximum
|
|
await expect(zai.answer(documents, 'Test question?', { chunkLength: 200_000 })).rejects.toThrow()
|
|
})
|
|
|
|
it('should process documents progressively with small chunkLength and track progress', async () => {
|
|
const documents = Array.from({ length: 100 }, (_, i) => `Entry ${i} about topic ${i}.`)
|
|
|
|
const result = zai.answer(documents, 'What topics are covered?', { chunkLength: 1500 })
|
|
|
|
let progressCount = 0
|
|
result.on('progress', (usage) => {
|
|
progressCount++
|
|
expect(usage.requests.percentage).toBeGreaterThanOrEqual(0)
|
|
expect(usage.requests.percentage).toBeLessThanOrEqual(1)
|
|
})
|
|
|
|
await result
|
|
|
|
// Should have emitted progress events for chunked processing
|
|
expect(progressCount).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle mixed document sizes with consistent chunkLength', async () => {
|
|
const documents = [
|
|
'Short doc.',
|
|
'Medium document with some content here. '.repeat(20),
|
|
'Very long document with extensive content. '.repeat(200),
|
|
'Another short one.',
|
|
'Medium sized content again. '.repeat(30),
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Summarize the content.', { chunkLength: 2000 })
|
|
|
|
expect(['answer', 'missing_knowledge']).toContain(result.type)
|
|
if (result.type === 'answer') {
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should handle chunkLength with code documents requiring preservation', async () => {
|
|
const codeDocs = Array.from({ length: 40 }, (_, i) => {
|
|
if (i === 15) {
|
|
return `function fibonacci(n) {\n if (n <= 1) return n;\n return fibonacci(n-1) + fibonacci(n-2);\n}`
|
|
}
|
|
return `// Comment ${i}\nconst var${i} = ${i};`
|
|
})
|
|
|
|
const result = await zai.answer(codeDocs, 'Show me the fibonacci function.', { chunkLength: 1000 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer).toMatch(/fibonacci/)
|
|
expect(result.citations.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('should maintain citation accuracy across chunk boundaries', async () => {
|
|
const documents = [
|
|
...Array.from({ length: 25 }, (_, i) => `Filler ${i}`),
|
|
'Important fact A: The capital of France is Paris.',
|
|
...Array.from({ length: 25 }, (_, i) => `Filler ${i + 25}`),
|
|
'Important fact B: Paris is located on the Seine River.',
|
|
...Array.from({ length: 25 }, (_, i) => `Filler ${i + 50}`),
|
|
'Important fact C: The Eiffel Tower is in Paris.',
|
|
...Array.from({ length: 25 }, (_, i) => `Filler ${i + 75}`),
|
|
]
|
|
|
|
const result = await zai.answer(documents, 'Tell me about Paris.', { chunkLength: 1200 })
|
|
|
|
expect(result.type).toBe('answer')
|
|
if (result.type === 'answer') {
|
|
expect(result.answer.toLowerCase()).toMatch(/paris/)
|
|
expect(result.citations.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Should cite the important facts, not filler
|
|
const citedItems = result.citations.map((c) => c.item as string)
|
|
const citedImportant = citedItems.filter((item) => item.includes('Important fact')).length
|
|
const citedFiller = citedItems.filter((item) => item.includes('Filler')).length
|
|
|
|
expect(citedImportant).toBeGreaterThan(0)
|
|
// Should primarily cite important facts
|
|
if (citedFiller > 0) {
|
|
expect(citedImportant).toBeGreaterThanOrEqual(citedFiller)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('abort functionality', () => {
|
|
it('can abort answer operation', async () => {
|
|
const documents = ['Document 1', 'Document 2', 'Document 3']
|
|
const request = zai.answer(documents, 'What is this about?')
|
|
|
|
request.abort('CANCEL')
|
|
await expect(request).rejects.toThrow(/cancel/i)
|
|
})
|
|
|
|
it('can abort via external signal', async () => {
|
|
const controller = new AbortController()
|
|
const documents = ['Some content here']
|
|
const request = zai.answer(documents, 'Question?').bindSignal(controller.signal)
|
|
|
|
controller.abort('CANCEL2')
|
|
await expect(request).rejects.toThrow(/cancel/i)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('zai.learn.answer', { timeout: 60_000, sequential: true }, () => {
|
|
const client = getClient()
|
|
const tableName = 'ZaiTestAnswerInternalTable'
|
|
const taskId = 'answer'
|
|
let zai = getZai()
|
|
|
|
beforeEach(async () => {
|
|
zai = getZai().with({
|
|
activeLearning: {
|
|
enable: true,
|
|
taskId,
|
|
tableName,
|
|
},
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await client.deleteTableRows({ table: tableName, deleteAllRows: true })
|
|
} catch (err) {}
|
|
})
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await client.deleteTable({ table: tableName })
|
|
} catch (err) {}
|
|
})
|
|
|
|
it('should use approved examples to improve answers', async () => {
|
|
const adapter = new TableAdapter({
|
|
client,
|
|
tableName,
|
|
})
|
|
|
|
const documents = ['Product X is great.', 'Product Y is also good.']
|
|
|
|
// Save an approved example with specific format
|
|
await adapter.saveExample<string, AnswerResult<string>>({
|
|
key: 'example1',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.answer',
|
|
instructions: '',
|
|
input: 'What products are mentioned?',
|
|
output: {
|
|
type: 'answer',
|
|
answer: 'The documents mention Product X and Product Y.',
|
|
citations: [
|
|
{ offset: 23, item: documents[0], snippet: 'Product X is great.' },
|
|
{ offset: 36, item: documents[1], snippet: 'Product Y is also good.' },
|
|
],
|
|
},
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
const result = await zai.learn(taskId).answer(documents, 'What products are discussed?')
|
|
|
|
// Should use the example to guide formatting
|
|
expect(['answer', 'ambiguous']).toContain(result.type)
|
|
})
|
|
|
|
it('should cache exact matches', async () => {
|
|
const documents = ['Cached content.']
|
|
const question = 'What is this?'
|
|
|
|
const first = await zai.learn(taskId).answer(documents, question)
|
|
const second = await zai.learn(taskId).answer(documents, question)
|
|
|
|
expect(first.type).toBe(second.type)
|
|
})
|
|
})
|