feat(server): improve context metadata & matching (#12064)

fix AI-20

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced file metadata with MIME type, blob ID, and file name across context and workspace, now visible in UI and API.
  - Added workspace-level matching for files and documents with configurable thresholds and workspace scoping in search queries.
  - Introduced a new error type and user-friendly messaging for global workspace context matching failures.

- **Bug Fixes**
  - Improved consistent handling of file MIME types and nullable context IDs for accurate metadata.

- **Documentation**
  - Updated GraphQL schema, queries, and mutations to include new metadata fields, optional parameters, and error types.

- **Style**
  - Added new localization strings for global context matching error messages.

- **Tests**
  - Extended test coverage with new and updated snapshot tests for metadata and matching logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
darkskygit
2025-05-14 06:32:29 +00:00
parent 04c5fd6dfc
commit cecf545590
36 changed files with 465 additions and 108 deletions
@@ -329,9 +329,12 @@ declare global {
abortSignal: AbortSignal
) => Promise<void>;
matchContext: (
contextId: string,
content: string,
limit?: number
contextId?: string,
workspaceId?: string,
limit?: number,
scopedThreshold?: number,
threshold?: number
) => Promise<{
files?: ContextMatchedFileChunk[];
docs?: ContextMatchedDocChunk[];
@@ -661,7 +661,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
>();
const { files: matchedFiles = [], docs: matchedDocs = [] } =
(await AIProvider.context?.matchContext(contextId, userInput)) ?? {};
(await AIProvider.context?.matchContext(userInput, contextId)) ?? {};
matchedDocs.forEach(doc => {
docContexts.set(doc.docId, {
@@ -241,7 +241,7 @@ export class CopilotClient {
sessionId,
},
});
return res.currentUser?.copilot?.contexts?.[0]?.id;
return res.currentUser?.copilot?.contexts?.[0]?.id || undefined;
}
async addContextDoc(options: OptionsField<typeof addContextDocMutation>) {
@@ -333,13 +333,23 @@ export class CopilotClient {
return res.currentUser?.copilot?.contexts?.[0];
}
async matchContext(contextId: string, content: string, limit?: number) {
async matchContext(
content: string,
contextId?: string,
workspaceId?: string,
limit?: number,
scopedThreshold?: number,
threshold?: number
) {
const res = await this.gql({
query: matchContextQuery,
variables: {
contextId,
content,
contextId,
workspaceId,
limit,
scopedThreshold,
threshold,
},
});
const { matchFiles: files, matchWorkspaceDocs: docs } =
@@ -700,11 +700,21 @@ Could you make a new website based on these notes and send back just the html fi
}
},
matchContext: async (
contextId: string,
content: string,
limit?: number
contextId?: string,
workspaceId?: string,
limit?: number,
scopedThreshold?: number,
threshold?: number
) => {
return client.matchContext(contextId, content, limit);
return client.matchContext(
content,
contextId,
workspaceId,
limit,
scopedThreshold,
threshold
);
},
});