Skip to content

Commit fb67945

Browse files
Merge pull request #200 from off-grid-ai/fix/ci-build-shared-rag
CI and release builds build the shared rag package
2 parents 1f63f03 + 99148d1 commit fb67945

9 files changed

Lines changed: 80 additions & 24 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ jobs:
135135
npm --prefix ../shared ci
136136
npm --prefix ../shared/packages/sync run build
137137
npm --prefix ../shared/packages/models run build
138+
# @offgrid/rag (the document engine) ships a built dist/ like sync and models.
139+
npm --prefix ../shared/packages/rag run build
138140
npm --prefix ../shared/packages/speech run build
139141
npm --prefix ../shared/packages/ui run build
140142
- name: Check out the private Desktop speech runtime
@@ -534,7 +536,7 @@ jobs:
534536
mv _shared ../shared
535537
mv _executorch_speech ../executorch-speech
536538
npm --prefix ../shared ci
537-
for package in models sync use automation speech ui; do
539+
for package in models rag sync use automation speech ui; do
538540
npm --prefix "../shared/packages/$package" run build
539541
done
540542
npm ci

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ jobs:
274274
npm --prefix ../shared/packages/use run build
275275
npm --prefix ../shared/packages/automation run build
276276
npm --prefix ../shared/packages/models run build
277+
# @offgrid/rag (the document engine) ships a built dist/ like sync and models.
278+
npm --prefix ../shared/packages/rag run build
277279
npm --prefix ../shared/packages/speech run build
278280
npm --prefix ../shared/packages/ui run build
279281
- name: Check out the private Desktop speech runtime
@@ -621,6 +623,8 @@ jobs:
621623
npm --prefix ../shared/packages/use run build
622624
npm --prefix ../shared/packages/automation run build
623625
npm --prefix ../shared/packages/models run build
626+
# @offgrid/rag (the document engine) ships a built dist/ like sync and models.
627+
npm --prefix ../shared/packages/rag run build
624628
npm --prefix ../shared/packages/speech run build
625629
npm --prefix ../shared/packages/ui run build
626630
- name: Check out the private Desktop speech package

‎.github/workflows/windows-build.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ jobs:
171171
mv _shared ../shared
172172
npm --prefix ../shared ci
173173
npm --prefix ../shared/packages/models run build
174+
# @offgrid/rag (the document engine) ships a built dist/ like sync and models.
175+
npm --prefix ../shared/packages/rag run build
174176
npm --prefix ../shared/packages/sync run build
175177
npm --prefix ../shared/packages/use run build
176178
npm --prefix ../shared/packages/automation run build

‎e2e/fixtures/chat-queue-versions-llama-server.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ const server = http.createServer((request, response) => {
7070
.join('\n')
7171
}))
7272
})
73-
if (prompt === 'Keep the saved answer after failure.' && attempt === 2) {
73+
// The resend fails, and so does the one retry the chat fallback makes for an unavailable model.
74+
if (prompt === 'Keep the saved answer after failure.' && (attempt === 2 || attempt === 3)) {
7475
response.writeHead(503, { 'Content-Type': 'application/json' })
7576
response.end(JSON.stringify({ error: { message: 'Synthetic model unavailable.' } }))
7677
return

‎src/main/__tests__/fixtures/synthetic-pdf.ts‎

Lines changed: 26 additions & 0 deletions
Large diffs are not rendered by default.

‎src/main/__tests__/rag-store-integration.dbtest.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import crypto from 'crypto'
1212
import fs from 'fs'
1313
import os from 'os'
1414
import path from 'path'
15-
import { SYNTHETIC_PDF } from './fixtures/synthetic-pdf'
15+
import { textLayerPdf } from './fixtures/synthetic-pdf'
1616

1717
const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-ragstore-it-'))
1818

@@ -271,7 +271,11 @@ describe('knowledge base over the document engine', () => {
271271

272272
it('reads a real PDF page by page', async () => {
273273
store.createProject({ id: 'p-pdf', name: 'PDF' })
274-
const pdf = await addFile('p-pdf', 'decision.pdf', SYNTHETIC_PDF)
274+
const pdf = await addFile(
275+
'p-pdf',
276+
'decision.pdf',
277+
textLayerPdf('Decision record: the launch moves to the second week of March after the audit.')
278+
)
275279

276280
const indexed = await finishedIndexing(pdf.documentId!)
277281
expect(indexed.state).toBe('ready')
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// ProjectDocumentsPort: a project's knowledge base as document sources. Every chat filed in the
22
// project uses these files alongside its own attachments; the rules are the shared turn service's.
3+
// Reads the knowledge-base rows (rag/store) directly: the knowledge-base service sits above this
4+
// layer and uses it, so importing the service here would be a cycle.
35

46
import fs from 'node:fs'
57
import type { DocumentSource, ProjectDocumentsPort } from '@offgrid/rag/documents'
@@ -8,10 +10,10 @@ import type { RagDocument } from '@offgrid/rag'
810
/** Kinds the engine reads from the file itself; the rest are indexed from their transcript or caption. */
911
const FILE_KINDS = new Set(['pdf', 'docx', 'text'])
1012

11-
async function sourceFor(document: RagDocument): Promise<DocumentSource | null> {
12-
const { knowledgeBase } = await import('../rag')
13-
// A file added before files had document ids is indexed in the background; it joins later turns.
14-
const id = await knowledgeBase.documentIdFor(document, FILE_KINDS.has(document.kind))
13+
function sourceFor(document: RagDocument): DocumentSource | null {
14+
// A file added before files had document ids has none until the knowledge-base list indexes it
15+
// (rag-ipc); it joins the turns from then on.
16+
const id = document.documentId
1517
if (!id) return null
1618
const onDisk = FILE_KINDS.has(document.kind) && fs.existsSync(document.path)
1719
return {
@@ -24,17 +26,17 @@ async function sourceFor(document: RagDocument): Promise<DocumentSource | null>
2426

2527
export const projectDocuments: ProjectDocumentsPort = {
2628
async forConversation(conversationId) {
27-
const { projectOfConversation } = await import('../rag')
29+
const { projectOfConversation } = await import('../rag/store')
2830
const projectId = projectOfConversation(conversationId)
2931
return projectId ? projectDocuments.forProject(projectId) : []
3032
},
3133
async forProject(projectId) {
32-
const { listRagDocuments } = await import('../rag')
34+
const { listRagDocuments } = await import('../rag/store')
3335
const sources: DocumentSource[] = []
3436
// Oldest first, so the stable prompt block keeps its order as files are added.
3537
for (const document of listRagDocuments(projectId).reverse()) {
3638
if (!document.enabled) continue
37-
const source = await sourceFor(document)
39+
const source = sourceFor(document)
3840
if (source) sources.push(source)
3941
}
4042
return sources
@@ -43,6 +45,6 @@ export const projectDocuments: ProjectDocumentsPort = {
4345

4446
/** Every document id a knowledge base holds, so cache cleanup never drops one. */
4547
export async function knowledgeBaseDocumentIds(): Promise<Set<string>> {
46-
const { listAllRagDocuments } = await import('../rag')
48+
const { listAllRagDocuments } = await import('../rag/store')
4749
return new Set(listAllRagDocuments().flatMap((document) => (document.documentId ? [document.documentId] : [])))
4850
}

‎src/main/documents/tool-extension.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,20 @@
55
// knowledge base and the chat's attachments.
66

77
import { describeTurnStatus } from '@offgrid/rag/documents'
8-
import type { ToolContext, ToolExtension } from '../tools'
8+
import type { SearchResult } from '../../shared/search-contract'
99
import { documentStore, documentTurns } from '.'
10-
import { foundPassageSource } from './chat-turn'
10+
import { foundPassageSource, type DocumentsActivity } from './chat-turn'
11+
12+
/**
13+
* What these tools read from the tool loop's context (fields of tools.ts's ToolContext). Declared
14+
* here, not imported: the tool loop imports this module, so importing it back would be a cycle.
15+
*/
16+
export interface DocumentToolContext {
17+
conversationId?: string
18+
projectId?: string
19+
citeDocumentPassage?: (source: SearchResult) => number
20+
onActivity?: (activity: DocumentsActivity) => void
21+
}
1122

1223
const SEARCH = 'search_documents'
1324
const READ = 'read_pages'
@@ -57,7 +68,7 @@ function where(label: string | undefined, page: number): string {
5768
}
5869

5970
/** The documents this tool call may see. */
60-
function scopeOf(context: ToolContext | undefined): { conversationId?: string; projectId?: string } {
71+
function scopeOf(context: DocumentToolContext | undefined): { conversationId?: string; projectId?: string } {
6172
return {
6273
...(context?.conversationId ? { conversationId: context.conversationId } : {}),
6374
...(context?.projectId ? { projectId: context.projectId } : {})
@@ -69,7 +80,7 @@ function scopeOf(context: ToolContext | undefined): { conversationId?: string; p
6980
* this turn's sources (also used by search_knowledge_base).
7081
*/
7182
export async function searchDocumentsForTool(
72-
context: ToolContext | undefined,
83+
context: DocumentToolContext | undefined,
7384
query: string,
7485
documentName?: string
7586
): Promise<string> {
@@ -87,14 +98,18 @@ export async function searchDocumentsForTool(
8798
return result.text.slice(0, MAX_CHARS)
8899
}
89100

90-
export const documentToolExtension: ToolExtension = {
101+
export const documentToolExtension = {
91102
id: 'attached-documents',
92-
category: 'tool',
103+
category: 'tool' as const,
93104
// Added per turn by toolChat when the conversation has documents, never from the shared list.
94-
schemas: () => [],
95-
canHandle: (name) => name === SEARCH || name === READ,
96-
isReadOnlyTool: () => true,
97-
async execute(name, args, context) {
105+
schemas: (): unknown[] => [],
106+
canHandle: (name: string): boolean => name === SEARCH || name === READ,
107+
isReadOnlyTool: (): boolean => true,
108+
async execute(
109+
name: string,
110+
args: Record<string, unknown>,
111+
context?: DocumentToolContext
112+
): Promise<string> {
98113
if (name === SEARCH) {
99114
const query = typeof args.query === 'string' ? args.query.trim() : ''
100115
if (!query) return 'Give a query to search for.'
@@ -122,6 +137,6 @@ export const documentToolExtension: ToolExtension = {
122137
.join('\n\n')
123138
.slice(0, MAX_CHARS)
124139
},
125-
systemHint: () =>
140+
systemHint: (): string =>
126141
'search_documents and read_pages look further into this chat\'s documents when the passages given are not enough.'
127142
}

‎src/renderer/src/components/VoiceSettingsTab.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ export function VoiceSettingsTab(): React.JSX.Element {
297297
'This is the Off Grid AI voice.',
298298
voices.length ? voice : undefined
299299
)
300-
if (!result?.dataUrl) throw new Error('No audio returned')
300+
if (!result.dataUrl) throw new Error('No audio returned')
301301
const audio = new Audio(result.dataUrl)
302302
testAudioRef.current = audio
303303
audio.playbackRate = preferences.speed

0 commit comments

Comments
 (0)