Skip to content

Commit 7b73f09

Browse files
committed
feat: Incorporate source code
1 parent c9b390d commit 7b73f09

File tree

1 file changed

+118
-74
lines changed
  • packages/cli/src/cmds/ask

1 file changed

+118
-74
lines changed

packages/cli/src/cmds/ask/ask.ts

Lines changed: 118 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
import { 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+
29
import { handleWorkingDirectory } from '../../lib/handleWorkingDirectory';
310
import { locateAppMapDir } from '../../lib/locateAppMapDir';
411
import { exists, verbose } from '../../utils';
5-
import OpenAI from 'openai';
6-
import { ChatCompletionMessageParam } from 'openai/resources';
712
import 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

2414
export const command = 'ask <question>';
2515
export const describe =
@@ -46,12 +36,12 @@ function buildOpenAI(): OpenAI {
4636
}
4737

4838
type 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

5747
type 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+
7067
class 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

Comments
 (0)