11import { warn } from 'console' ;
2+ import OpenAI from 'openai' ;
3+ import { ChatCompletionMessageParam } from 'openai/resources' ;
4+ import { readFile } from 'fs/promises' ;
5+ import { join } from 'path' ;
6+ import { AppMapFilter , Event , Metadata , buildAppMap } from '@appland/models' ;
7+ import { Action , Specification , buildDiagram , nodeName } from '@appland/sequence-diagram' ;
8+
29import { handleWorkingDirectory } from '../../lib/handleWorkingDirectory' ;
310import { locateAppMapDir } from '../../lib/locateAppMapDir' ;
411import { exists , verbose } from '../../utils' ;
5- import OpenAI from 'openai' ;
6- import { ChatCompletionMessageParam } from 'openai/resources' ;
712import Context from '../../inspect/context' ;
8- import { EventMatch } from '../../search/types' ;
9- import { readFile , writeFile } from 'fs/promises' ;
10- import { join } from 'path' ;
11- import { AppMapFilter , Metadata , buildAppMap } from '@appland/models' ;
12- import { loadSequenceDiagram } from '../compare/loadSequenceDiagram' ;
13- import {
14- Action ,
15- Diagram ,
16- FormatType ,
17- Specification ,
18- buildDiagram ,
19- format ,
20- nodeName ,
21- } from '@appland/sequence-diagram' ;
22- import { match } from 'assert' ;
2313
2414export const command = 'ask <question>' ;
2515export const describe =
@@ -46,12 +36,12 @@ function buildOpenAI(): OpenAI {
4636}
4737
4838type SearchDiagramsParam = {
49- keywords : string [ ] ;
39+ keyword : string ;
5040} ;
5141
52- type DiagramEvent = {
53- name : string ;
54- id : number ;
42+ type ActionInfo = {
43+ eventIds ? : string ;
44+ location ?: string ;
5545} ;
5646
5747type SearchDiagramResult = {
@@ -67,15 +57,29 @@ type DiagramDetailsResult = {
6757 summary : string ;
6858} ;
6959
60+ type Reference = {
61+ id : number ;
62+ diagramId : string ;
63+ sourceLocation ?: string ;
64+ eventIds ?: number [ ] ;
65+ } ;
66+
7067class Ask {
7168 constructor ( public appmapDir : string ) { }
7269
73- async searchDiagrams ( keywords : string [ ] ) : Promise < SearchDiagramResult [ ] | undefined > {
70+ async searchDiagrams ( keyword : string ) : Promise < SearchDiagramResult [ ] | undefined > {
7471 const regexpOfGlob = ( glob : string ) =>
7572 glob . replace ( / \. / g, '\\.' ) . replace ( / \* / g, '.*' ) . replace ( / \? / g, '.' ) ;
7673
7774 // TODO: Use proper keyword search
78- const searchTerms = keywords . map ( ( keyword ) => keyword . split ( ' ' ) ) . flat ( ) ;
75+ const searchTerms = [ keyword ] ;
76+ // .map((keyword) => keyword.split(' '))
77+ // .flat()
78+ // .map((kw) => {
79+ // if (kw.startsWith('+')) return kw.substring(1);
80+ // if (kw.startsWith('-')) return kw.substring(1);
81+ // return kw;
82+ // });
7983 const matchesForKeyword = new Array < Set < string > > ( ) ;
8084 for ( const keyword of searchTerms ) {
8185 matchesForKeyword . push ( new Set < string > ( ) ) ;
@@ -85,31 +89,9 @@ class Ask {
8589 if ( ! context . codeObjectMatches ) continue ;
8690
8791 const stats = await context . buildStats ( ) ;
88- /*
89- const eventName = (eventMatch: EventMatch): string => {
90- if (eventMatch.event.sqlQuery) return eventMatch.event.sqlQuery;
91- else if (eventMatch.event.httpServerRequest)
92- return eventMatch.event.httpServerRequest.normalized_path;
93-
94- return [eventMatch.event.definedClass, eventMatch.event.methodId].join('#');
95- };
96- */
97-
9892 stats . eventMatches . forEach ( ( eventMatch ) => {
9993 const diagramId = eventMatch . appmap ;
10094 matchesForKeyword [ matchesForKeyword . length - 1 ] . add ( diagramId ) ;
101- /*
102- const event: DiagramEvent = {
103- name: eventName(eventMatch),
104- id: eventMatch.event.id,
105- };
106- const eventMatches = eventMatchesByDiagramId.get(diagramId);
107- if (eventMatches) {
108- eventMatches.push(event);
109- } else {
110- eventMatchesByDiagramId.set(diagramId, [event]);
111- }
112- */
11395 } ) ;
11496 }
11597
@@ -127,10 +109,6 @@ class Ask {
127109 }
128110
129111 return Array . from ( matchingDiagrams ) . map ( ( diagramId ) => ( { diagramId } ) ) ;
130- // return Array.from(eventMatchesByDiagramId.entries()).map(([diagramId, matchingEvents]) => ({
131- // diagramId,
132- // matchingEvents,
133- // }));
134112 }
135113}
136114
@@ -148,9 +126,9 @@ export const handler = async (argv: any) => {
148126
149127 async function searchDiagrams ( paramStr : string ) : Promise < SearchDiagramResult [ ] > {
150128 const params = JSON . parse ( paramStr ) as SearchDiagramsParam ;
151- let { keywords } = params ;
152- warn ( `Searching for diagrams matching "${ keywords } "` ) ;
153- const eventMatches = await ask . searchDiagrams ( keywords ) ;
129+ let { keyword } = params ;
130+ warn ( `Searching for diagrams matching "${ keyword } "` ) ;
131+ const eventMatches = await ask . searchDiagrams ( keyword ) ;
154132 if ( ! eventMatches ) return [ ] ;
155133
156134 return eventMatches ;
@@ -178,6 +156,10 @@ export const handler = async (argv: any) => {
178156
179157 filter . declutter . limitRootEvents . on = true ;
180158 const filteredAppMap = filter . filter ( prunedAppMap , [ ] ) ;
159+ const eventsById = filteredAppMap . events . reduce ( ( map , event ) => {
160+ map . set ( event . id , event ) ;
161+ return map ;
162+ } , new Map < number , Event > ( ) ) ;
181163 const specification = Specification . build ( filteredAppMap , { loops : true } ) ;
182164 const diagram = buildDiagram ( appmapFile , filteredAppMap , specification ) ;
183165
@@ -186,8 +168,28 @@ export const handler = async (argv: any) => {
186168 const collectAction = ( action : Action , depth = 0 ) => {
187169 if ( depth > maxDepth ) return ;
188170
171+ const actionInfo : ActionInfo = { } ;
172+ if ( action . eventIds . length > 0 ) {
173+ actionInfo . eventIds = action . eventIds . join ( ',' ) ;
174+ const co = eventsById . get ( action . eventIds [ 0 ] ) ?. codeObject ;
175+ if ( co ) {
176+ actionInfo . location = co . location ;
177+ } else {
178+ warn ( `No code object for event ${ action . eventIds [ 0 ] } ` ) ;
179+ }
180+ }
181+ const actionInfoStr = Object . keys ( actionInfo )
182+ . sort ( )
183+ . map ( ( key ) => {
184+ const value = actionInfo [ key ] ;
185+ return `${ key } =${ value } ` ;
186+ } )
187+ . join ( ',' ) ;
188+
189189 const indent = ' ' . repeat ( depth ) ;
190- diagramText . push ( `${ indent } ${ nodeName ( action ) } (events:${ action . eventIds . join ( ', ' ) } })` ) ;
190+ diagramText . push (
191+ `${ indent } ${ nodeName ( action ) } ${ actionInfoStr !== '' ? ` (${ actionInfoStr } )` : '' } `
192+ ) ;
191193 if ( action . children ) {
192194 action . children . forEach ( ( child ) => collectAction ( child , depth + 1 ) ) ;
193195 }
@@ -198,19 +200,60 @@ export const handler = async (argv: any) => {
198200 return result ;
199201 }
200202
203+ async function lookupSourceCode ( locationStr : string ) : Promise < string | undefined > {
204+ const params = JSON . parse ( locationStr ) as { location : string } ;
205+
206+ const [ path , lineno ] = params . location . split ( ':' ) ;
207+ if ( ! ( await exists ( path ) ) ) return ;
208+
209+ const fileContent = await readFile ( path , 'utf-8' ) ;
210+ if ( ! lineno ) return fileContent ;
211+
212+ const languageRegexMap : Record < string , RegExp > = {
213+ '.rb' : new RegExp ( `def\\s+\\w+.*?\\n(.*?\\n)*?^end\\b` , 'gm' ) ,
214+ '.java' : new RegExp (
215+ `(?:public|private|protected)?\\s+(?:static\\s+)?(?:final\\s+)?(?:synchronized\\s+)?(?:abstract\\s+)?(?:native\\s+)?(?:strictfp\\s+)?(?:transient\\s+)?(?:volatile\\s+)?(?:\\w+\\s+)*\\w+\\s+\\w+\\s*\\([^)]*\\)\\s*(?:throws\\s+\\w+(?:,\\s*\\w+)*)?\\s*\\{(?:[^{}]*\\{[^{}]*\\})*[^{}]*\\}` ,
216+ 'gm'
217+ ) ,
218+ '.py' : new RegExp ( `def\\s+\\w+.*?:\\n(.*?\\n)*?` , 'gm' ) ,
219+ '.js' : new RegExp (
220+ `(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{(?:[^{}]*\\{[^{}]*\\})*[^{}]*\\}` ,
221+ 'gm'
222+ ) ,
223+ } ;
224+
225+ const extension = path . substring ( path . lastIndexOf ( '.' ) ) ;
226+ const regex = languageRegexMap [ extension ] ;
227+
228+ if ( regex ) {
229+ const match = regex . exec ( fileContent ) ;
230+ if ( match ) {
231+ const lines = match [ 0 ] . split ( '\n' ) ;
232+ const startLine = parseInt ( lineno , 10 ) ;
233+ const endLine = startLine + lines . length - 1 ;
234+ if ( startLine <= endLine ) {
235+ return lines . slice ( startLine - 1 , endLine ) . join ( '\n' ) ;
236+ }
237+ }
238+ }
239+ }
240+
201241 const question = argv . question ;
202242
203243 const systemMessages : ChatCompletionMessageParam [ ] = [
204244 'You are an assistant that answers questions about the design and architecture of code.' ,
205245 'You answer these questions by accessing a knowledge base of sequence diagrams.' ,
206246 'Each sequence diagram conists of a series of events, such as function calls, HTTP server requests, SQL queries, etc.' ,
207- 'You are given a question about the code, and you have to (a) Locate the sequence diagram that best answers the question (b) Focus the sequence diagram on the sub-section of the diagram that best answers the question.' ,
208- 'You use the function calling interface to choose the best diagram and configure the diagram view to reveal the answer.' ,
209- 'Always try multiple keywords, and collate the results together' ,
210- 'If a diagram keyword search returns no diagrams, try other keywords.' ,
211- 'The first function you should call is the "showPlan" function, to which you will provide a markdown document describing your general strategy for answering the question.' ,
212- 'In your response, include links to the diagram files, with anchor tags to events that are particular interest' ,
213- 'In your response, include links to source code files, with anchor tags to the lines of code that are of particular interest' ,
247+ 'First, call the "showPlan" function with a Markdown document that describes your strategy for answering the question.' ,
248+ 'Then, call the "searchDiagrams" function with an array of keywords to search for in the diagrams.' ,
249+ 'If "searchDiagrams" fails, continue calling "searchDiagrams" with different keywords and synonyms of keywords until you find a match.' ,
250+ 'Call "searchDiagrams" with only one keyword at a time' ,
251+ 'Next, call the "getDiagramDetails" function with an array of diagram ids to get details about the diagrams.' ,
252+ 'Evaluate which diagrams are most relevant to the question.' ,
253+ 'Enhance your answer by using "lookupSourceCode" function to get the source code for the most relevant functions.' ,
254+ 'Finally, respond with a Markdown document that summarizes the diagrams and answers the question.' ,
255+ 'Subsequent mentions of the function should use backticks but should not be links.' ,
256+ 'Never emit phrases like "note that the actual behavior may vary between different applications"' ,
214257 ] . map ( ( msg ) => ( {
215258 content : msg ,
216259 role : 'system' ,
@@ -248,12 +291,8 @@ export const handler = async (argv: any) => {
248291 parameters : {
249292 type : 'object' ,
250293 properties : {
251- keywords : {
252- type : 'array' ,
253- description : 'Array of keywords to find in the diagrams' ,
254- items : {
255- type : 'string' ,
256- } ,
294+ keyword : {
295+ type : 'string' ,
257296 } ,
258297 } ,
259298 } ,
@@ -274,6 +313,19 @@ export const handler = async (argv: any) => {
274313 } ,
275314 } ,
276315 } ,
316+ {
317+ function : lookupSourceCode ,
318+ description : `Get the source code for a specific function.` ,
319+ parameters : {
320+ type : 'object' ,
321+ properties : {
322+ location : {
323+ type : 'string' ,
324+ description : `Source code location in the format <path>[:<line number>]. Line number can be omitted if it's not known` ,
325+ } ,
326+ } ,
327+ } ,
328+ } ,
277329 ] ,
278330 } ) ;
279331
@@ -296,12 +348,4 @@ export const handler = async (argv: any) => {
296348 return ;
297349 }
298350 console . log ( response ) ;
299-
300- // const context = new Context(appmapDir, codeObjectId);
301- // await context.findCodeObjects();
302- // if (context.codeObjectMatches?.length === 0) {
303- // return yargs.exit(1, new Error(`Code object '${context.codeObjectId}' not found`));
304- // }
305- // await context.buildStats();
306- // console.log(JSON.stringify(context.stats, null, 2));
307351} ;
0 commit comments