From 355d931458dded962a186de86722c451e75c530d Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 16 Jan 2025 15:20:32 -0500 Subject: [PATCH] fix: Include pinned files in the content considered by vector terms Fixes #2199 --- .../welcome-message-v1.md | 0 architecture/navie/file-content-fetcher.md | 70 ++++++++++++++++++ packages/navie/src/agent.ts | 14 ++-- packages/navie/src/agents/explain-agent.ts | 1 + packages/navie/src/agents/gatherer.ts | 7 +- packages/navie/src/interaction-history.ts | 34 ++++++++- .../src/services/apply-context-service.ts | 10 ++- .../navie/src/services/context-service.ts | 34 +++++++-- .../src/services/file-content-fetcher.ts | 6 +- .../src/services/lookup-context-service.ts | 13 +++- packages/navie/src/services/memory-service.ts | 18 +++-- .../src/services/project-info-service.ts | 29 ++++++-- .../src/services/vector-terms-service.ts | 72 +------------------ packages/navie/test/agent-options.spec.ts | 26 +++++++ .../navie/test/agents/diagram-agent.spec.ts | 6 +- .../navie/test/agents/explain-agent.spec.ts | 6 +- packages/navie/test/agents/gatherer.spec.ts | 2 + .../navie/test/interaction-history.spec.ts | 19 ++++- .../services/apply-context-service.spec.ts | 13 +++- .../test/services/context-service.spec.ts | 19 +++-- 20 files changed, 288 insertions(+), 111 deletions(-) rename architecture/{navie-chat => navie-ui}/welcome-message-v1.md (100%) create mode 100644 architecture/navie/file-content-fetcher.md diff --git a/architecture/navie-chat/welcome-message-v1.md b/architecture/navie-ui/welcome-message-v1.md similarity index 100% rename from architecture/navie-chat/welcome-message-v1.md rename to architecture/navie-ui/welcome-message-v1.md diff --git a/architecture/navie/file-content-fetcher.md b/architecture/navie/file-content-fetcher.md new file mode 100644 index 0000000000..aef3673af6 --- /dev/null +++ b/architecture/navie/file-content-fetcher.md @@ -0,0 +1,70 @@ +File content fetcher is used to detect the locations of files that are mentioned in the chat, and to +fetch the content of those files. + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant GenerateAgent as GenerateAgent + participant TestAgent as TestAgent + participant ClientRequest as ClientRequest + participant ChatHistory as ChatHistory + participant FileContentFetcher as FileContentFetcher + participant FileChangeExtractorService as FileChangeExtractorService + participant ContextService as ContextService + + alt Invocation by GenerateAgent + GenerateAgent->>FileContentFetcher: applyFileContext(options, options.chatHistory) + end + + alt Invocation by TestAgent + TestAgent->>FileContentFetcher: applyFileContext(options, options.chatHistory) + end + + activate FileContentFetcher + FileContentFetcher->>FileChangeExtractorService: listFiles(clientRequest, chatHistory) + activate FileChangeExtractorService + FileChangeExtractorService-->>FileContentFetcher: fileNames + deactivate FileChangeExtractorService + + alt FileNames Found + FileContentFetcher->>ContextService: locationContext(fileNames) + activate ContextService + ContextService-->>FileContentFetcher: Context updated + deactivate ContextService + else No FileNames + FileContentFetcher-->>ClientRequest: return undefined + end + + deactivate FileContentFetcher +``` + +### Class Map + +```mermaid +classDiagram + class FileContentFetcher { + +applyFileContext(clientRequest: ClientRequest, chatHistory: ChatHistory | undefined): void + } + + class FileChangeExtractorService { + +listFiles(clientRequest: ClientRequest, chatHistory: ChatHistory | undefined): string[] + } + + class ContextService { + +locationContext(fileNames: string[]): void + } + + class GenerateAgent { + +applyFileContext(options: AgentOptions, chatHistory: ChatHistory | undefined): void + } + + class TestAgent { + +applyFileContext(options: AgentOptions, chatHistory: ChatHistory | undefined): void + } + + GenerateAgent --> FileContentFetcher : use + TestAgent --> FileContentFetcher : use + FileContentFetcher --> FileChangeExtractorService : relies on + FileContentFetcher --> ContextService : interacts with +``` diff --git a/packages/navie/src/agent.ts b/packages/navie/src/agent.ts index 3dd0961dd7..80f05b3aee 100644 --- a/packages/navie/src/agent.ts +++ b/packages/navie/src/agent.ts @@ -29,21 +29,25 @@ export class AgentOptions { return this.projectInfo.some((info) => info.appmapStats && info.appmapStats?.numAppMaps > 0); } - get contextLocations(): string[] { + get pinnedFileLocations(): string[] { return this.codeSelection && typeof this.codeSelection !== 'string' ? this.codeSelection.filter(UserContext.hasLocation).map((cs) => cs.location) : []; } + /** + * Configure context filters to fetch the content that's relevant to the current user options, + * including /include and /exclude options and pinned file locations. + */ buildContextFilters(): ContextV2.ContextFilters { const filters: ContextV2.ContextFilters = {}; if (this.contextLabels) filters.labels = this.contextLabels; this.userOptions.populateContextFilters(filters); - const contextLocations = this.contextLocations; - if (contextLocations.length > 0) { - filters.locations = contextLocations; + const pinnedFileLocations = this.pinnedFileLocations; + if (pinnedFileLocations.length > 0) { + filters.locations = pinnedFileLocations; filters.exclude ||= []; - filters.exclude.push(...contextLocations); + filters.exclude.push(...pinnedFileLocations); } return filters; } diff --git a/packages/navie/src/agents/explain-agent.ts b/packages/navie/src/agents/explain-agent.ts index cf561d7357..75732981e5 100644 --- a/packages/navie/src/agents/explain-agent.ts +++ b/packages/navie/src/agents/explain-agent.ts @@ -293,6 +293,7 @@ export default class ExplainAgent implements Agent { ); await this.contextService.locationContextFromOptions(options); + if ( hasLabel(options.contextLabels, ContextV2.ContextLabelName.Overview)?.weight !== ContextV2.ContextLabelWeight.High diff --git a/packages/navie/src/agents/gatherer.ts b/packages/navie/src/agents/gatherer.ts index 77e501a052..8243b8b856 100644 --- a/packages/navie/src/agents/gatherer.ts +++ b/packages/navie/src/agents/gatherer.ts @@ -5,6 +5,7 @@ import { debug as makeDebug } from 'node:util'; import { ContextItemEvent, + ContextItemRequestor, type InteractionEvent, PromptInteractionEvent, } from '../interaction-history'; @@ -64,7 +65,11 @@ export default class Gatherer { let response = ''; if (locations.length > 0 || terms.length > 0) { - for (const event of await this.context.searchContextWithLocations(terms, locations)) { + for (const event of await this.context.searchContextWithLocations( + ContextItemRequestor.Gatherer, + terms, + locations + )) { const location = event.location?.startsWith(event.directory ?? '') ? event.location : [event.directory, event.location].filter(Boolean).join('/'); diff --git a/packages/navie/src/interaction-history.ts b/packages/navie/src/interaction-history.ts index d71c8f2756..03a73dd9c5 100644 --- a/packages/navie/src/interaction-history.ts +++ b/packages/navie/src/interaction-history.ts @@ -211,9 +211,19 @@ export class HelpLookupEvent extends InteractionEvent { } } +export enum ContextItemRequestor { + Gatherer = 'gatherer', + Memory = 'memory', + Mentions = 'mentions', + PinnedFile = 'pinnedFile', + ProjectInfo = 'projectInfo', + Terms = 'terms', +} + export class ContextItemEvent extends InteractionEvent { constructor( public promptType: PromptType, + public requestor: ContextItemRequestor, public content: string, public location?: string | undefined, public directory?: string | undefined @@ -286,16 +296,24 @@ export class TechStackEvent extends InteractionEvent { class InteractionHistory extends EventEmitter { public readonly events: InteractionEvent[] = []; + private acceptPinnedFileContext = true; + // eslint-disable-next-line class-methods-use-this log(message: string) { - console.log(message); + console.warn(message); } addEvent(event: InteractionEvent) { + if (!this.validateEvent(event)) return; + this.emit('event', event); this.events.push(event); } + stopAcceptingPinnedFileContext() { + this.acceptPinnedFileContext = false; + } + clear() { this.events.splice(0, this.events.length); } @@ -328,6 +346,20 @@ class InteractionHistory extends EventEmitter { return state; } + + protected validateEvent(event: InteractionEvent) { + if (event instanceof ContextItemEvent) { + if (event.requestor === ContextItemRequestor.PinnedFile && !this.acceptPinnedFileContext) { + this.log( + 'WARNING Unexpected pinned file context item; no further pinned file context is expected.' + ); + // This is a warning only. We don't want to stop the event from being added. + return true; + } + } + + return true; + } } interface InteractionHistory { diff --git a/packages/navie/src/services/apply-context-service.ts b/packages/navie/src/services/apply-context-service.ts index 751c166806..f3dd238232 100644 --- a/packages/navie/src/services/apply-context-service.ts +++ b/packages/navie/src/services/apply-context-service.ts @@ -3,6 +3,7 @@ import { warn } from 'console'; import { ContextV2 } from '../context'; import InteractionHistory, { ContextItemEvent, + ContextItemRequestor, PromptInteractionEvent, } from '../interaction-history'; import { PromptType, buildPromptDescriptor } from '../prompt'; @@ -13,6 +14,7 @@ export default class ApplyContextService { constructor(public readonly interactionHistory: InteractionHistory) {} applyContext( + requestor: ContextItemRequestor, context: ContextV2.ContextResponse | undefined, help: HelpResponse, characterLimit: number, @@ -44,7 +46,7 @@ export default class ApplyContextService { const charsRemaining = characterLimit - charsApplied; for (const item of appliedContextItems) { - const event = eventOfContextItem(item); + const event = eventOfContextItem(requestor, item); if (event) this.interactionHistory.addEvent(event); } @@ -102,7 +104,10 @@ export default class ApplyContextService { } } -export function eventOfContextItem(item: ContextV2.ContextItem): undefined | ContextItemEvent { +export function eventOfContextItem( + requestor: ContextItemRequestor, + item: ContextV2.ContextItem +): undefined | ContextItemEvent { let promptType: PromptType | undefined; switch (item.type) { case ContextV2.ContextItemType.SequenceDiagram: @@ -125,6 +130,7 @@ export function eventOfContextItem(item: ContextV2.ContextItem): undefined | Con const isFile = ContextV2.isFileContextItem(item); return new ContextItemEvent( promptType, + requestor, item.content, isFile ? item.location : undefined, isFile ? item.directory : undefined diff --git a/packages/navie/src/services/context-service.ts b/packages/navie/src/services/context-service.ts index bef20521b1..b7aa8640ea 100644 --- a/packages/navie/src/services/context-service.ts +++ b/packages/navie/src/services/context-service.ts @@ -4,7 +4,7 @@ import transformSearchTerms from '../lib/transform-search-terms'; import LookupContextService from './lookup-context-service'; import VectorTermsService from './vector-terms-service'; import { ContextV2 } from '../context'; -import InteractionHistory, { ContextItemEvent } from '../interaction-history'; +import InteractionHistory, { ContextItemEvent, ContextItemRequestor } from '../interaction-history'; import ApplyContextService, { eventOfContextItem } from './apply-context-service'; export default class ContextService { @@ -15,6 +15,9 @@ export default class ContextService { private applyContextService: ApplyContextService ) {} + /** + * Populates the interaction history with context obtained by searching the project. + */ async searchContext( options: AgentOptions, tokensAvailable: () => number, @@ -25,6 +28,18 @@ export default class ContextService { if (contextEnabled) { this.history.log('[context-service] Searching for context'); + this.history.stopAcceptingPinnedFileContext(); + + const aggregateQuestion = [...options.aggregateQuestion]; + // Add content obtained from pinned files + for (const event of this.history.events) { + if (!(event instanceof ContextItemEvent)) continue; + + if (!(event.requestor === ContextItemRequestor.PinnedFile)) continue; + + aggregateQuestion.push(event.content); + } + const searchTerms = await transformSearchTerms( termsEnabled, options.aggregateQuestion, @@ -47,7 +62,13 @@ export default class ContextService { } } - async locationContext(fileNames: string[]): Promise { + /** + * Populates the interaction history with file contents of the provided file names. + */ + async locationContext( + requestor: ContextItemRequestor, + fileNames: string[] + ): Promise { if (!fileNames || fileNames.length === 0) { this.history.log('[context-service] No file names provided for location context'); return []; @@ -66,8 +87,9 @@ export default class ContextService { let charsAdded = 0; const events: ContextItemEvent[] = []; for (const item of context) { - const contextItem = eventOfContextItem(item); + const contextItem = eventOfContextItem(requestor, item); if (!contextItem) continue; + charsAdded += contextItem.content.length; events.push(contextItem); this.history.addEvent(contextItem); @@ -77,6 +99,7 @@ export default class ContextService { } async searchContextWithLocations( + requestor: ContextItemRequestor, searchTerms: string[], fileNames: string[] ): Promise { @@ -88,7 +111,7 @@ export default class ContextService { let charsAdded = 0; const events: ContextItemEvent[] = []; for (const item of ContextService.guardContextType(context)) { - const contextItem = eventOfContextItem(item); + const contextItem = eventOfContextItem(requestor, item); if (!contextItem) continue; charsAdded += contextItem.content.length; events.push(contextItem); @@ -102,8 +125,7 @@ export default class ContextService { const locations = options.buildContextFilters().locations ?? []; // Also list project directories locations.unshift(':0'); - console.log(locations); - await this.locationContext(locations); + await this.locationContext(ContextItemRequestor.PinnedFile, locations); } static guardContextType( diff --git a/packages/navie/src/services/file-content-fetcher.ts b/packages/navie/src/services/file-content-fetcher.ts index f5c2234366..2c59826e9f 100644 --- a/packages/navie/src/services/file-content-fetcher.ts +++ b/packages/navie/src/services/file-content-fetcher.ts @@ -1,7 +1,11 @@ +import { ContextItemRequestor } from '../interaction-history'; import { ClientRequest, ChatHistory } from '../navie'; import ContextService from './context-service'; import FileChangeExtractorService from './file-change-extractor-service'; +/** + * Used to detect file mentions within a chat interaction and to fetch their content using helper services. + */ export default class FileContentFetcher { constructor( private fileChangeExtractor: FileChangeExtractorService, @@ -17,6 +21,6 @@ export default class FileContentFetcher { return undefined; } - await this.contextService.locationContext(fileNames); + await this.contextService.locationContext(ContextItemRequestor.Mentions, fileNames); } } diff --git a/packages/navie/src/services/lookup-context-service.ts b/packages/navie/src/services/lookup-context-service.ts index f9b0837ecb..6bf2a6c0bd 100644 --- a/packages/navie/src/services/lookup-context-service.ts +++ b/packages/navie/src/services/lookup-context-service.ts @@ -1,5 +1,9 @@ import { warn } from 'console'; -import InteractionHistory, { ContextLookupEvent, HelpLookupEvent } from '../interaction-history'; +import InteractionHistory, { + ContextItemRequestor, + ContextLookupEvent, + HelpLookupEvent, +} from '../interaction-history'; import { ContextV2 } from '../context'; import { CHARACTERS_PER_TOKEN } from '../message'; import ApplyContextService from './apply-context-service'; @@ -67,6 +71,11 @@ export default class LookupContextService { ) { applyContextService.addSystemPrompts(context, help); - applyContextService.applyContext(context, help, tokenCount * CHARACTERS_PER_TOKEN); + applyContextService.applyContext( + ContextItemRequestor.Terms, + context, + help, + tokenCount * CHARACTERS_PER_TOKEN + ); } } diff --git a/packages/navie/src/services/memory-service.ts b/packages/navie/src/services/memory-service.ts index debfa2b6f0..5e44c3cae4 100644 --- a/packages/navie/src/services/memory-service.ts +++ b/packages/navie/src/services/memory-service.ts @@ -1,8 +1,12 @@ import { ConversationSummaryMemory } from 'langchain/memory'; -import { AIMessage, HumanMessage } from '@langchain/core/messages'; - import { BaseLanguageModelInterface } from '@langchain/core/language_models/base'; -import { ContextItemEvent, InteractionEvent, PromptInteractionEvent } from '../interaction-history'; + +import { + ContextItemEvent, + ContextItemRequestor, + InteractionEvent, + PromptInteractionEvent, +} from '../interaction-history'; import Message from '../message'; import { PromptType, buildPromptDescriptor } from '../prompt'; import { convertToMessage } from './completion-service'; @@ -29,7 +33,7 @@ export class LangchainMemoryService implements MemoryService { 'system', buildPromptDescriptor(PromptType.ConversationSummary) ), - new ContextItemEvent(PromptType.ConversationSummary, summary), + new ContextItemEvent(PromptType.ConversationSummary, ContextItemRequestor.Memory, summary), ]; } } @@ -44,7 +48,11 @@ export const NaiveMemoryService: MemoryService = { 'system', buildPromptDescriptor(PromptType.ConversationSummary) ), - new ContextItemEvent(PromptType.ConversationSummary, concatenatedMessages), + new ContextItemEvent( + PromptType.ConversationSummary, + ContextItemRequestor.Memory, + concatenatedMessages + ), ]; }, }; diff --git a/packages/navie/src/services/project-info-service.ts b/packages/navie/src/services/project-info-service.ts index d9aab7ff42..47e564cca0 100644 --- a/packages/navie/src/services/project-info-service.ts +++ b/packages/navie/src/services/project-info-service.ts @@ -10,9 +10,10 @@ import { } from '../project-info'; import InteractionHistory, { ContextItemEvent, + ContextItemRequestor, PromptInteractionEvent, } from '../interaction-history'; -import { PromptType, buildPromptDescriptor, buildPromptValue } from '../prompt'; +import { PromptType, buildPromptDescriptor } from '../prompt'; type Test = () => boolean; @@ -74,12 +75,17 @@ export default class ProjectInfoService { ); if (appmapConfigs.length > 0) { this.interactionHistory.addEvent( - new ContextItemEvent(PromptType.AppMapConfig, dump(appmapConfigs)) + new ContextItemEvent( + PromptType.AppMapConfig, + ContextItemRequestor.ProjectInfo, + dump(appmapConfigs) + ) ); } else { this.interactionHistory.addEvent( new ContextItemEvent( PromptType.AppMapConfig, + ContextItemRequestor.ProjectInfo, 'The project does not contain an AppMap config file (appmap.yml). This file is automatically generated by the AppMap recording agent and does not need to be created by the user.' ) ); @@ -94,11 +100,19 @@ export default class ProjectInfoService { ); if (appmapStats.map((stats) => stats.numAppMaps).reduce((a, b) => a + b, 0) > 0) { this.interactionHistory.addEvent( - new ContextItemEvent(PromptType.AppMapStats, dump(appmapStats)) + new ContextItemEvent( + PromptType.AppMapStats, + ContextItemRequestor.ProjectInfo, + dump(appmapStats) + ) ); } else { this.interactionHistory.addEvent( - new ContextItemEvent(PromptType.AppMapStats, 'The project does not contain any AppMaps.') + new ContextItemEvent( + PromptType.AppMapStats, + ContextItemRequestor.ProjectInfo, + 'The project does not contain any AppMaps.' + ) ); } @@ -113,12 +127,17 @@ export default class ProjectInfoService { this.interactionHistory.addEvent( new ContextItemEvent( PromptType.CodeEditor, + ContextItemRequestor.ProjectInfo, dump(codeEditors.map(({ name }) => ({ name, installed: true, activated: true }))) ) ); } else { this.interactionHistory.addEvent( - new ContextItemEvent(PromptType.CodeEditor, 'The code editor is not specified.') + new ContextItemEvent( + PromptType.CodeEditor, + ContextItemRequestor.ProjectInfo, + 'The code editor is not specified.' + ) ); } } diff --git a/packages/navie/src/services/vector-terms-service.ts b/packages/navie/src/services/vector-terms-service.ts index 38b5752e85..1e8cbfb079 100644 --- a/packages/navie/src/services/vector-terms-service.ts +++ b/packages/navie/src/services/vector-terms-service.ts @@ -28,75 +28,6 @@ The developer asks a question using natural language. This question must be conv The search terms should be single words and underscore_separated_words.`; -const promptExamples: Message[] = [ - { - content: 'How do I record AppMap data of my Spring app?', - role: 'user', - }, - { - content: JSON.stringify({ - context: 'Record AppMap data of Spring', - instructions: 'How to do it', - terms: ['record', 'AppMap', 'data', 'Java', '+Spring'], - }), - role: 'assistant', - }, - - { - content: 'How does the user login handle the case when the password is invalid?', - role: 'user', - }, - { - content: JSON.stringify({ - context: 'User login handle password validation invalid error', - instructions: 'Explain how this is handled by the code', - terms: ['user', 'login', 'handle', '+password', 'validate', 'invalid', 'error'], - }), - role: 'assistant', - }, - - { - content: - 'Can you describe in detail usage of redis in flow of GET /test-group/test-project-1/-/blob/main/README.md request with code snippets?', - role: 'user', - }, - { - content: JSON.stringify({ - context: 'Redis GET /test-group/test-project-1/-/blob/main/README.md', - instructions: 'Describe in detail with code snippets', - terms: ['+Redis', 'get', 'test-group', 'test-project-1', 'blob', 'main', 'README'], - }), - role: 'assistant', - }, - - { - content: - 'Create test cases of the logContext function using jest. Follow established patterns for mocking with jest.', - role: 'user', - }, - { - content: JSON.stringify({ - context: 'logContext jest test case', - instructions: 'Create test cases, following established patterns for mocking with jest.', - terms: ['test', 'cases', '+logContext', 'log', 'context', 'jest'], - }), - role: 'assistant', - }, - - { - content: 'auth', - role: 'user', - }, - { - content: JSON.stringify({ - context: 'auth authentication authorization', - instructions: 'Describe the authentication and authorization process', - terms: ['+auth', 'authentication', 'authorization', 'token', 'strategy', 'provider'], - }), - role: 'assistant', - }, -]; - const schema = z.object({ context: z.string(), instructions: z.string(), @@ -115,7 +46,6 @@ export default class VectorTermsService { content: SYSTEM_PROMPT, role: 'system', }, - ...promptExamples, { content: question, role: 'user', @@ -123,7 +53,7 @@ export default class VectorTermsService { ]; const response = await this.completionsService.json(messages, schema, { - model: this.completionsService.miniModelName, + model: this.completionsService.modelName, }); debug(`Vector terms response: ${JSON.stringify(response, undefined, 2)}`); diff --git a/packages/navie/test/agent-options.spec.ts b/packages/navie/test/agent-options.spec.ts index 360e36d541..bf5b8681a8 100644 --- a/packages/navie/test/agent-options.spec.ts +++ b/packages/navie/test/agent-options.spec.ts @@ -1,3 +1,4 @@ +import { UserContext } from '../src/user-context'; import { AgentOptions } from '../src/agent'; import { UserOptions } from '../src/lib/parse-options'; @@ -31,4 +32,29 @@ describe('AgentOptions', () => { expect(filters).toEqual({ locations: ['file1.md'], exclude: ['file1.md'] }); }); }); + + describe('pinned file inclusion', () => { + const pinnedFiles: UserContext.LocationItem[] = [ + { + location: 'file1.md', + type: 'file', + }, + ]; + + it('includes pinned file locations in the filters', () => { + const userOptions = new UserOptions(new Map()); + const agentOptions = new AgentOptions(question, question, userOptions, [], [], pinnedFiles); + + const filters = agentOptions.buildContextFilters(); + expect(filters.locations).toContain('file1.md'); + }); + + it('excludes pinned file locations', () => { + const userOptions = new UserOptions(new Map()); + const agentOptions = new AgentOptions(question, question, userOptions, [], [], pinnedFiles); + + const filters = agentOptions.buildContextFilters(); + expect(filters.exclude).toContain('file1.md'); + }); + }); }); diff --git a/packages/navie/test/agents/diagram-agent.spec.ts b/packages/navie/test/agents/diagram-agent.spec.ts index c648089c2c..9abd0b57fe 100644 --- a/packages/navie/test/agents/diagram-agent.spec.ts +++ b/packages/navie/test/agents/diagram-agent.spec.ts @@ -1,4 +1,7 @@ -import InteractionHistory, { PromptInteractionEvent } from '../../src/interaction-history'; +import InteractionHistory, { + ContextItemRequestor, + PromptInteractionEvent, +} from '../../src/interaction-history'; import { AgentOptions } from '../../src/agent'; import ContextService from '../../src/services/context-service'; import DiagramAgent, { DIAGRAM_AGENT_PROMPT } from '../../src/agents/diagram-agent'; @@ -90,6 +93,7 @@ describe('@diagram agent', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(contextService.locationContext).toHaveBeenCalledWith( + ContextItemRequestor.PinnedFile, expect.arrayContaining(['file1', 'file2']) ); }); diff --git a/packages/navie/test/agents/explain-agent.spec.ts b/packages/navie/test/agents/explain-agent.spec.ts index eaa15c34da..47b7fd5ec6 100644 --- a/packages/navie/test/agents/explain-agent.spec.ts +++ b/packages/navie/test/agents/explain-agent.spec.ts @@ -1,5 +1,8 @@ import ExplainAgent from '../../src/agents/explain-agent'; -import InteractionHistory, { isPromptEvent } from '../../src/interaction-history'; +import InteractionHistory, { + ContextItemRequestor, + isPromptEvent, +} from '../../src/interaction-history'; import ApplyContextService from '../../src/services/apply-context-service'; import VectorTermsService from '../../src/services/vector-terms-service'; import LookupContextService from '../../src/services/lookup-context-service'; @@ -89,6 +92,7 @@ describe('@explain agent', () => { expect(lookupContextService.lookupHelp).not.toHaveBeenCalled(); expect(applyContextService.applyContext).toHaveBeenCalledWith( + ContextItemRequestor.Terms, context, [], tokensAvailable * CHARACTERS_PER_TOKEN diff --git a/packages/navie/test/agents/gatherer.spec.ts b/packages/navie/test/agents/gatherer.spec.ts index 184efea66e..db340555e4 100644 --- a/packages/navie/test/agents/gatherer.spec.ts +++ b/packages/navie/test/agents/gatherer.spec.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import Gatherer from '../../src/agents/gatherer'; import { ContextItemEvent, + ContextItemRequestor, type InteractionEvent, PromptInteractionEvent, } from '../../src/interaction-history'; @@ -182,6 +183,7 @@ describe('Gatherer', () => { case 'contextItem': return new ContextItemEvent( (String(ev.promptType) ?? 'test') as never, + ContextItemRequestor.Gatherer, String(ev.content), ev.location as never, ev.directory as never diff --git a/packages/navie/test/interaction-history.spec.ts b/packages/navie/test/interaction-history.spec.ts index bce10dd2cc..17c2f6d077 100644 --- a/packages/navie/test/interaction-history.spec.ts +++ b/packages/navie/test/interaction-history.spec.ts @@ -1,6 +1,7 @@ import { ContextV2 } from '../src/context'; import InteractionHistory, { ContextItemEvent, + ContextItemRequestor, ContextLookupEvent, } from '../src/interaction-history'; import { PromptType } from '../src/prompt'; @@ -50,7 +51,11 @@ describe('InteractionHistory', () => { it('adds a message to the state', () => { const interactionHistory = new InteractionHistory(); interactionHistory.addEvent( - new ContextItemEvent(PromptType.SequenceDiagram, 'diagram-1') + new ContextItemEvent( + PromptType.SequenceDiagram, + ContextItemRequestor.Terms, + 'diagram-1' + ) ); const state = interactionHistory.buildState(); expect(state.messages).toEqual([ @@ -64,7 +69,12 @@ describe('InteractionHistory', () => { it('adds a message to the state', () => { const interactionHistory = new InteractionHistory(); interactionHistory.addEvent( - new ContextItemEvent(PromptType.CodeSnippet, 'code snippet content', 'file.py') + new ContextItemEvent( + PromptType.CodeSnippet, + ContextItemRequestor.Terms, + 'code snippet content', + 'file.py' + ) ); const state = interactionHistory.buildState(); expect(state.messages).toEqual([ @@ -83,6 +93,7 @@ describe('InteractionHistory', () => { interactionHistory.addEvent( new ContextItemEvent( PromptType.CodeSnippet, + ContextItemRequestor.Terms, 'code snippet content', location, projectDirectory @@ -102,7 +113,9 @@ describe('InteractionHistory', () => { describe('data request', () => { it('adds a message to the state', () => { const interactionHistory = new InteractionHistory(); - interactionHistory.addEvent(new ContextItemEvent(PromptType.DataRequest, 'data request')); + interactionHistory.addEvent( + new ContextItemEvent(PromptType.DataRequest, ContextItemRequestor.Terms, 'data request') + ); const state = interactionHistory.buildState(); expect(state.messages).toEqual([ { content: '', role: 'system' }, diff --git a/packages/navie/test/services/apply-context-service.spec.ts b/packages/navie/test/services/apply-context-service.spec.ts index 8ce65b45be..403f9a6778 100644 --- a/packages/navie/test/services/apply-context-service.spec.ts +++ b/packages/navie/test/services/apply-context-service.spec.ts @@ -1,6 +1,9 @@ import { ContextV2 } from '../../src/context'; import { HelpProvider, HelpRequest, HelpResponse } from '../../src/help'; -import InteractionHistory, { ContextItemEvent } from '../../src/interaction-history'; +import InteractionHistory, { + ContextItemEvent, + ContextItemRequestor, +} from '../../src/interaction-history'; import ApplyContextService from '../../src/services/apply-context-service'; import LookupContextService from '../../src/services/lookup-context-service'; import { HELP_CONTEXT, SEARCH_CONTEXT } from '../fixture'; @@ -21,7 +24,13 @@ describe('ApplyContextService', () => { afterEach(() => jest.resetAllMocks()); const collect = (characterLimit: number, maxContentLength = characterLimit / 5) => - applyContextService.applyContext(context, help, characterLimit, maxContentLength); + applyContextService.applyContext( + ContextItemRequestor.Terms, + context, + help, + characterLimit, + maxContentLength + ); it('collects samples of context into the output', () => { collect(1000 * 1000); diff --git a/packages/navie/test/services/context-service.spec.ts b/packages/navie/test/services/context-service.spec.ts index 80adf7aa95..5e60272a3b 100644 --- a/packages/navie/test/services/context-service.spec.ts +++ b/packages/navie/test/services/context-service.spec.ts @@ -8,7 +8,10 @@ import ApplyContextService from '../../src/services/apply-context-service'; import { UserOptions } from '../../src/lib/parse-options'; import { SEARCH_CONTEXT } from '../fixture'; import { ContextV2 } from '../../src/context'; -import InteractionHistory, { ContextItemEvent } from '../../src/interaction-history'; +import InteractionHistory, { + ContextItemEvent, + ContextItemRequestor, +} from '../../src/interaction-history'; describe('ContextService', () => { let history: InteractionHistory; @@ -110,7 +113,12 @@ describe('ContextService', () => { await contextService.searchContext(options, () => tokensAvailable); - expect(applyContextService.applyContext).toHaveBeenCalledWith([], [], 3000); + expect(applyContextService.applyContext).toHaveBeenCalledWith( + ContextItemRequestor.Terms, + [], + [], + 3000 + ); }); }); }); @@ -132,7 +140,7 @@ describe('ContextService', () => { it('retrieves context for files', async () => { lookupContextService.lookupContext = jest.fn().mockResolvedValue(locationContext); - await contextService.locationContext(['file1', 'file2']); + await contextService.locationContext(ContextItemRequestor.Terms, ['file1', 'file2']); expect(lookupContextService.lookupContext).toHaveBeenCalledWith([], 0, { locations: ['file1', 'file2'], @@ -151,7 +159,7 @@ describe('ContextService', () => { ]); }); - it('sets the directory field on ContextItemEvent', async () => { + it('sets the directory and requestor on ContextItemEvent', async () => { const locationContextWithDirectory = [ { type: ContextV2.ContextItemType.CodeSnippet, @@ -164,11 +172,12 @@ describe('ContextService', () => { .fn() .mockResolvedValue(locationContextWithDirectory); - await contextService.locationContext(['file1']); + await contextService.locationContext(ContextItemRequestor.Terms, ['file1']); const event = history.events[0]; assert(event instanceof ContextItemEvent); expect(event.directory).toEqual('dir1'); + expect(event.requestor).toEqual(ContextItemRequestor.Terms); }); }); });