mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
fix(ui): list callers only for the selected symbol and only on list questions
Without a detected subject every caller question counted as one about the selection, and subjects were found only for camelCase names. "Who calls get_queryset?" or "Wer ruft as_sql auf?" therefore listed the callers of the selected JSONBAgg. The question is now read around its verb, so snake_case and single-capital names are subjects too, and only a subject that names the selection, one of its roots or "it"/"this" gets the graph list. The direction patterns also missed common phrasings and read "Welche Funktion ruft X auf?" as a callee question. Singular "welche <Nomen> ruft X auf", "rufen ... auf", "wo wird X aufgerufen", "which/what <noun> call X" and "where is X called" now ask for callers; a plural noun with "ruft", "was ruft" and "wird von X aufgerufen" ask for callees. Why, how, what-if and explain questions go to the model even when they mention callers. A listed answer now offers "Ask the model", which sends the same question with the same evidence as a fresh request and replaces the list. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
@@ -903,6 +903,7 @@ describe('graph answers and answer limits', () => {
|
||||
expect(answer).toContain('not generated by the model');
|
||||
expect(container.querySelector('.cbm-chat-answer .cbm-chat-source-content')?.textContent).toContain('Incoming relationships: 23 from 12 symbols.');
|
||||
expect([...container.querySelectorAll('button')].some(item => item.textContent === 'Retry')).toBe(false);
|
||||
expect(button('Ask the model')).toBeDefined();
|
||||
expect(container.querySelector('textarea')?.value).toBe('');
|
||||
await type('Which of them are tests?'); await click('Send ↑');
|
||||
const request = runtime.chat.mock.calls[0][0];
|
||||
@@ -910,6 +911,22 @@ describe('graph answers and answer limits', () => {
|
||||
expect(request[2].content).toContain('test_values_list');
|
||||
});
|
||||
|
||||
it('hands a listed answer to the model on request, with its evidence and without the list as history', async () => {
|
||||
const { props, runtime } = fixture();
|
||||
await render({ ...props, proactiveSelection: jsonbAggEvidence() }); await click('Download & load');
|
||||
await type('Who calls JSONBAgg?'); await click('Send ↑');
|
||||
expect(runtime.chat).not.toHaveBeenCalled();
|
||||
await click('Ask the model');
|
||||
expect(runtime.chat).toHaveBeenCalledOnce();
|
||||
const request = runtime.chat.mock.calls[0][0];
|
||||
expect(request.map(message => message.role)).toEqual(['system', 'user']);
|
||||
expect(request[1].content).toContain('Who calls JSONBAgg?');
|
||||
expect(request[1].content).toContain('Incoming relationships: 23 from 12 symbols.');
|
||||
expect(container.querySelectorAll('.cbm-chat-turn')).toHaveLength(1);
|
||||
expect(container.querySelector('.cbm-chat-answer-text')?.textContent).toBe('Adds the two values.');
|
||||
expect(button('Retry')).toBeDefined();
|
||||
});
|
||||
|
||||
// 40 callers with long names in their own files: the snapshot names 24, a default prompt fewer.
|
||||
const longCallers = () => {
|
||||
const callers = Array.from({ length: 40 }, (_, index) => ({ id: 2000 + index, name: `caller_with_a_long_descriptive_name_number_${index}`, label: 'Function',
|
||||
|
||||
@@ -361,7 +361,9 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
const nextContext = selectedGraph ? [...selectedContext.filter(item => item.id !== selectedGraph.id), selectedGraph] : selectedContext;
|
||||
const extra = (retry ? retry.context ?? [] : nextContext).map(item => ({ ...item }));
|
||||
let packet = retry?.evidence;
|
||||
const currentGraph = !reader && proactiveSelection ? [{ ...proactiveSelection }] : [];
|
||||
// A listed answer asked again goes to the model: the same question and evidence in a fresh request.
|
||||
const ask = retry?.answeredFrom === 'graph';
|
||||
const currentGraph = !retry && !reader && proactiveSelection ? [{ ...proactiveSelection }] : [];
|
||||
const consume = (): void => {
|
||||
setDraft(previous => previous === prompt ? '' : previous);
|
||||
setSelectedContext(previous => previous.filter(item => !extra.some(sent => sent.id === item.id && sent.text === item.text)));
|
||||
@@ -378,10 +380,11 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
consume();
|
||||
return;
|
||||
}
|
||||
let history: ChatTurn[] = turns;
|
||||
const earlier = ask ? turns.filter(item => item.id !== retry.id) : turns;
|
||||
let history: ChatTurn[] = earlier;
|
||||
const makeRequest = () => buildChatMessages(history, prompt, source, extra, reader, currentGraph, packet ? formatExplanationEvidence(packet) : undefined);
|
||||
if (!retry && ((reader?.source?.text.length ?? 0) > 5000 || currentGraph.length)) packet = prepareExplanationContext(reader, currentGraph, 3200);
|
||||
let request = retry ? retry.request.map(message => ({ ...message })) : makeRequest();
|
||||
let request = retry && !ask ? retry.request.map(message => ({ ...message })) : makeRequest();
|
||||
const queued = { cancelled: false }; manualRequest.current = queued;
|
||||
const waitingEpoch = epoch.current;
|
||||
if (automaticRun) {
|
||||
@@ -402,7 +405,7 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
if (epoch.current !== ticket || stopRequested.current) return;
|
||||
const limit = Math.min(limits.inputTokens, model.contextTokens - limits.outputTokens);
|
||||
// Earlier turns give way first, oldest first; the question and its evidence stay.
|
||||
while (!retry && count > limit && history.length) {
|
||||
while ((!retry || ask) && count > limit && history.length) {
|
||||
const characters = request.reduce((sum, message) => sum + message.content.length, 0);
|
||||
history = trimChatHistory(history, Math.ceil((count - limit) * characters / Math.max(1, count)));
|
||||
request = makeRequest(); count = await currentRuntime.countTokens(request);
|
||||
@@ -418,8 +421,8 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
return;
|
||||
}
|
||||
const id = retry?.id ?? `local-turn-${crypto.randomUUID()}`;
|
||||
const historyOmitted = retry ? retry.historyOmitted
|
||||
: turns.filter(item => item.status !== 'error' && item.status !== 'generating' && !history.includes(item)).length;
|
||||
const historyOmitted = retry && !ask ? retry.historyOmitted
|
||||
: earlier.filter(item => item.status !== 'error' && item.status !== 'generating' && !history.includes(item)).length;
|
||||
const turn: ChatTurn = { id, prompt, attachment: source, readerContext: reader, context: extra, evidence: packet, modelId: model.id, request, answer: '', status: 'generating',
|
||||
...historyOmitted ? { historyOmitted } : {} };
|
||||
activeTurn.current = id;
|
||||
@@ -553,7 +556,7 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
{turn.status === 'stopped' && turn.answer && <small>Stopped · partial answer</small>}
|
||||
{turn.status !== 'generating' && turn.answeredFrom !== 'graph' && <AnswerNotes shortened={turn.shortened} packet={turn.evidence} model={BROWSER_MODELS.find(candidate => candidate.id === turn.modelId)?.displayName ?? turn.modelId} historyOmitted={turn.historyOmitted} />}
|
||||
{turn.status === 'error' && <p className="cbm-chat-turn-error" role="alert">{turn.error}</p>}
|
||||
{index === turns.length - 1 && turn.status !== 'generating' && turn.answeredFrom !== 'graph' && <button type="button" className="cbm-chat-retry" disabled={phase !== 'ready'} onClick={() => { void send(turn); }}>Retry</button>}
|
||||
{index === turns.length - 1 && turn.status !== 'generating' && <button type="button" className="cbm-chat-retry" disabled={phase !== 'ready'} onClick={() => { void send(turn); }}>{turn.answeredFrom === 'graph' ? browserChatText.askModel : 'Retry'}</button>}
|
||||
</div>
|
||||
</article>)}
|
||||
</div>}
|
||||
|
||||
@@ -7,22 +7,41 @@ const line = (markdown: string, type: string) => markdown.split('\n').find(item
|
||||
|
||||
describe('caller and callee questions', () => {
|
||||
it.each([
|
||||
['Who calls JSONBAgg?', ['incoming'], 'en'],
|
||||
['Who calls JSONBAgg? List every caller and the edge type.', ['incoming'], 'en'],
|
||||
['List the callers of this class', ['incoming'], 'en'],
|
||||
['Where is it called from? Show what calls it.', ['incoming'], 'en'],
|
||||
['What is JSONBAgg called by?', ['incoming'], 'en'],
|
||||
['What does JSONBAgg call?', ['outgoing'], 'en'],
|
||||
['Show the callees', ['outgoing'], 'en'],
|
||||
['Wer ruft JSONBAgg auf?', ['incoming'], 'de'],
|
||||
['Welche Aufrufer hat die Klasse?', ['incoming'], 'de'],
|
||||
['Was ruft JSONBAgg auf?', ['outgoing'], 'de'],
|
||||
['Who calls it and what does it call?', ['incoming', 'outgoing'], 'en'],
|
||||
] as const)('recognizes %s', (prompt, sides, language) => {
|
||||
expect(relationshipQuestion(prompt)).toEqual({ sides, language });
|
||||
['Who calls JSONBAgg?', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['Who calls JSONBAgg? List every caller and the edge type.', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['List the callers of this class', ['incoming'], 'en', undefined],
|
||||
['Where is it called from? Show what calls it.', ['incoming'], 'en', undefined],
|
||||
['What is JSONBAgg called by?', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['Which functions call JSONBAgg?', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['What functions call JSONBAgg?', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['Where is JSONBAgg called?', ['incoming'], 'en', 'JSONBAgg'],
|
||||
['How many callers does JSONBAgg have?', ['incoming'], 'en', undefined],
|
||||
['What does JSONBAgg call?', ['outgoing'], 'en', 'JSONBAgg'],
|
||||
['Which functions does the JSONBAgg class call?', ['outgoing'], 'en', 'JSONBAgg'],
|
||||
['Show the functions called by JSONBAgg', ['outgoing'], 'en', 'JSONBAgg'],
|
||||
['Show the callees', ['outgoing'], 'en', undefined],
|
||||
['Wer ruft JSONBAgg auf?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Wer ruft die Funktion auf?', ['incoming'], 'de', undefined],
|
||||
['Welche Aufrufer hat die Klasse?', ['incoming'], 'de', undefined],
|
||||
['Welche Funktion ruft JSONBAgg auf?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Welche Funktionen rufen JSONBAgg auf?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Welche Tests rufen JSONBAgg auf?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Wo wird JSONBAgg aufgerufen?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Von wem wird JSONBAgg aufgerufen?', ['incoming'], 'de', 'JSONBAgg'],
|
||||
['Was ruft JSONBAgg auf?', ['outgoing'], 'de', 'JSONBAgg'],
|
||||
['Welche Funktionen ruft JSONBAgg auf?', ['outgoing'], 'de', 'JSONBAgg'],
|
||||
['Was wird von JSONBAgg aufgerufen?', ['outgoing'], 'de', 'JSONBAgg'],
|
||||
['Who calls it and what does it call?', ['incoming', 'outgoing'], 'en', undefined],
|
||||
['Who calls get_queryset?', ['incoming'], 'en', 'get_queryset'],
|
||||
['Wer ruft as_sql auf?', ['incoming'], 'de', 'as_sql'],
|
||||
['What does handle call?', ['outgoing'], 'en', 'handle'],
|
||||
] as const)('recognizes %s', (prompt, sides, language, subject) => {
|
||||
expect(relationshipQuestion(prompt)).toEqual({ sides, language, ...subject ? { subject } : {} });
|
||||
});
|
||||
|
||||
it.each(['Explain JSONBAgg', 'What does this class do?', 'Was macht diese Klasse?', 'Write a test for JSONBAgg'])('leaves %s to the model', prompt => {
|
||||
it.each(['Explain JSONBAgg', 'What does this class do?', 'Was macht diese Klasse?', 'Write a test for JSONBAgg',
|
||||
'Explain how the callers use JSONBAgg', 'Why does the caller pass distinct=True?', 'What would break for callers if I rename JSONBAgg?',
|
||||
'Who calls JSONBAgg and why?', 'Warum ruft der Test JSONBAgg auf?', 'Wie wird JSONBAgg von den Tests aufgerufen?'])('leaves %s to the model', prompt => {
|
||||
expect(relationshipQuestion(prompt)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -92,7 +111,10 @@ describe('listed relationship answers', () => {
|
||||
});
|
||||
|
||||
it('leaves questions about another symbol, attached non-Galaxy evidence and other questions to the model', () => {
|
||||
expect(relationshipAnswer('Who calls BaseCommand?', [jsonbAggEvidence()])).toBeUndefined();
|
||||
for (const prompt of ['Who calls BaseCommand?', 'Who calls get_queryset?', 'Wer ruft as_sql auf?', 'Who calls Aggregate?', 'What does handle call?',
|
||||
'How many callers does get_queryset have?']) expect(relationshipAnswer(prompt, [jsonbAggEvidence()])).toBeUndefined();
|
||||
expect(relationshipAnswer('Who calls the JSONBAgg class?', [jsonbAggEvidence()])).toBeDefined();
|
||||
expect(relationshipAnswer('Wer ruft django.contrib.postgres.aggregates.general.JSONBAgg auf?', [jsonbAggEvidence()])).toBeDefined();
|
||||
expect(relationshipAnswer('Who calls JSONBAgg?', [{ id: 'g', label: 'Callers', text: '{"kind":"graph-context-snapshot"}' }])).toBeUndefined();
|
||||
expect(relationshipAnswer('Explain JSONBAgg', [jsonbAggEvidence()])).toBeUndefined();
|
||||
expect(relationshipAnswer('Who calls it?', [jsonbAggEvidence()])).toBeDefined();
|
||||
|
||||
@@ -1,29 +1,94 @@
|
||||
import { classifyQuestion, identifierNamesIn, mentionsIn, quotedNamesIn } from '../compiler/question-classifier';
|
||||
import { identifierNamesIn, mentionsIn, quotedNamesIn } from '../compiler/question-classifier';
|
||||
import type { BrowserChatContext } from './chat-model';
|
||||
import { readGalaxyEvidence, relationshipLine, scopeSentence, sideLoaded, type GalaxyEvidence } from './galaxy-evidence';
|
||||
import { relationshipWords } from './strings';
|
||||
|
||||
export interface RelationshipQuestion { sides: ('incoming' | 'outgoing')[]; language: 'en' | 'de' }
|
||||
|
||||
/** "What does X call" asks for callees; the shared classifier does not have that class. */
|
||||
const CALLEE_PATTERNS = [/\bwhat\s+(?:does|do|did|will)\s+.+?\s+call\b/i, /\bwhich\s+\w+\s+(?:does|do|did)\s+.+?\s+call\b/i,
|
||||
/\bcallees?\b/i, /\bwas\s+ruft\b/i, /\bwelche\s+\w+\s+ruft\b/i, /\bwas\s+wird\s+von\b.+\baufgerufen\b/i];
|
||||
const CALLER_PATTERNS = [/\bwhat\s+calls\b/i, /\bcallers?\b/i, /\bwer\s+ruft\b/i, /\bvon\s+wem\b/i];
|
||||
const GERMAN = /\b(?:wer|welche\w*|ruft|rufen|aufrufer|aufgerufen|wem|wird)\b/i;
|
||||
|
||||
/** Caller and callee questions, in English or German. Anything else goes to the model. */
|
||||
export function relationshipQuestion(prompt: string): RelationshipQuestion | undefined {
|
||||
const callees = CALLEE_PATTERNS.some(pattern => pattern.test(prompt));
|
||||
const callers = CALLER_PATTERNS.some(pattern => pattern.test(prompt)) || classifyQuestion(prompt).klass === 'who-calls';
|
||||
if (!callers && !callees) return undefined;
|
||||
return { sides: [...callers ? ['incoming' as const] : [], ...callees ? ['outgoing' as const] : []], language: GERMAN.test(prompt) ? 'de' : 'en' };
|
||||
type Side = 'incoming' | 'outgoing';
|
||||
export interface RelationshipQuestion {
|
||||
sides: Side[];
|
||||
language: 'en' | 'de';
|
||||
/** The symbol named where the question puts its subject; undefined for "it", "this" or none. */
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
/** A question that names another symbol is not about this selection. */
|
||||
function aboutSelection(prompt: string, evidence: GalaxyEvidence): boolean {
|
||||
const named = [...mentionsIn(prompt), ...quotedNamesIn(prompt), ...identifierNamesIn(prompt)];
|
||||
const selected = new Set([evidence.label, ...evidence.roots.map(root => root.name)].map(name => name.toLowerCase()));
|
||||
return !named.length || named.some(name => selected.has(name.toLowerCase()) || selected.has(name.split('.').pop()!.toLowerCase()));
|
||||
const KIND = 'function|method|class|symbol|file|module|funktion|methode|klasse|datei|modul';
|
||||
/** Where a pattern says {name}: an optional article and kind noun, the name as typed, an optional kind noun. */
|
||||
const NAME = String.raw`(?:(?:the|a|an|der|die|das|den|dem|des)\s+)?(?:(?:${KIND})\s+)?[@\x60'"]?([A-Za-z_][\w.:/]*)[\x60'"]?(?:\s+(?:${KIND})\b)?`;
|
||||
const pattern = (source: string) => new RegExp(source.replace('{name}', NAME), 'iu');
|
||||
|
||||
/** Phrasings that fix the direction and, where it is written, the subject. */
|
||||
const PHRASINGS: { side: Side; pattern: RegExp }[] = [
|
||||
...[
|
||||
String.raw`\bwho\s+(?:else\s+)?(?:calls|invokes|uses)\s+{name}`,
|
||||
String.raw`\bwhat\s+(?:calls|invokes)\s+{name}`,
|
||||
String.raw`\b(?:which|what)\s+\w+\s+(?:calls?|invokes?)\s+{name}`,
|
||||
String.raw`\bcall(?:ers?|\s+sites?)\s+(?:of|for|to)\s+{name}`,
|
||||
String.raw`\bwhere\s+(?:is|are|does|do)\s+{name}\s+(?:\w+\s+)?(?:called|invoked|used)\b`,
|
||||
String.raw`\b(?:is|are)\s+{name}\s+(?:called|invoked|used)\s+by\b`,
|
||||
String.raw`\bwer\s+(?:ruft|benutzt|verwendet|nutzt)\s+{name}`,
|
||||
String.raw`\bwelche\w*\s+[\p{L}\w]+\s+(?:rufen|benutzen|verwenden|nutzen)\s+{name}`,
|
||||
String.raw`\b(?:wo|woher|von\s+wo)(?:\s+überall)?\s+wird\s+{name}\s+(?:\w+\s+)?(?:aufgerufen|benutzt|verwendet|genutzt)\b`,
|
||||
String.raw`\bvon\s+wem\s+(?:wird|werden)\s+{name}`,
|
||||
String.raw`\baufrufer\s+(?:von|für)\s+{name}`,
|
||||
String.raw`\bwird\s+{name}\s+(?:\w+\s+)?aufgerufen\b`,
|
||||
].map(source => ({ side: 'incoming' as const, pattern: pattern(source) })),
|
||||
...[
|
||||
String.raw`\bwhat\s+(?:does|do|did|will|can)\s+{name}\s+(?:call|invoke|use)\b`,
|
||||
String.raw`\b(?:which|what)\s+\w+\s+(?:does|do|did|will|can)\s+{name}\s+(?:call|invoke|use)\b`,
|
||||
String.raw`\bcallees?\s+(?:of|for)\s+{name}`,
|
||||
String.raw`\bcalled\s+by\s+{name}`,
|
||||
String.raw`\bwas\s+ruft\s+{name}`,
|
||||
String.raw`\b(?:wird|werden)\s+von\s+{name}\s+(?:\w+\s+)?aufgerufen\b`,
|
||||
].map(source => ({ side: 'outgoing' as const, pattern: pattern(source) })),
|
||||
];
|
||||
/** "Welche Funktion ruft X auf" asks for callers; with a plural noun, "ruft" has X as subject. */
|
||||
const WHICH_CALLS = pattern(String.raw`\b(welche[rsnm]?)\s+([\p{L}\w]+)\s+ruft\s+{name}`);
|
||||
/** Direction words without a written subject. */
|
||||
const GENERAL: { side: Side; pattern: RegExp }[] = [
|
||||
{ side: 'incoming', pattern: /\b(?:callers?|call\s+sites?|who\s+(?:calls|invokes|uses)|what\s+calls|wer\s+ruft|von\s+wem|aufrufer)\b/i },
|
||||
{ side: 'outgoing', pattern: /\b(?:callees?|was\s+ruft)\b/i },
|
||||
];
|
||||
/** Why, how, what-if and explain questions want reasoning, not a list; "how many" still asks for one. */
|
||||
const REASONING = /\b(?:why|how(?!\s+(?:many|often))|explain\w*|describe\w*|would|could|should|if|when|rename\w*|break\w*|impact\w*|warum|wieso|weshalb|wie(?!\s+(?:viele|oft))|erkl(?:ä|ae)r\w*|beschreib\w*|passiert|wäre|waere|würde|wuerde|könnte|koennte|sollte|wenn|falls|umbenenn\w*|auswirkung\w*)\b/iu;
|
||||
const GERMAN = /\b(?:wer|welche\w*|ruft|rufen|aufrufer|aufgerufen|wem|wird|werden)\b/i;
|
||||
/** Subjects that mean the current selection. */
|
||||
const SELF = new Set(['it', 'this', 'that', 'these', 'those', 'here', 'es', 'dies', 'diese', 'dieser', 'dieses', 'diesen', 'diesem', 'sie', 'ihn',
|
||||
'function', 'method', 'class', 'symbol', 'file', 'module', 'selection', 'funktion', 'methode', 'klasse', 'datei', 'modul', 'auswahl']);
|
||||
/** Words a {name} can land on that are not a subject at all; the match is discarded. */
|
||||
const NOT_A_NAME = new Set(['in', 'into', 'from', 'at', 'on', 'to', 'for', 'with', 'of', 'and', 'or', 'by', 'anything', 'anyone', 'something', 'someone',
|
||||
'what', 'which', 'who', 'whom', 'all', 'every', 'auf', 'von', 'im', 'aus', 'und', 'oder', 'irgendwo', 'überhaupt', 'etwas', 'jemand',
|
||||
'was', 'wer', 'wem', 'wen', 'alle', 'jede', 'jeder', 'jedes']);
|
||||
|
||||
/** Caller and callee questions that ask for a list, in English or German. Anything else goes to the model. */
|
||||
export function relationshipQuestion(prompt: string): RelationshipQuestion | undefined {
|
||||
if (REASONING.test(prompt)) return undefined;
|
||||
const sides = new Set<Side>();
|
||||
let subject: string | undefined, named = false;
|
||||
const take = (side: Side, name: string | undefined): void => {
|
||||
const word = name?.replace(/[.:/]+$/, '');
|
||||
if (!word || NOT_A_NAME.has(word.toLowerCase())) return;
|
||||
sides.add(side);
|
||||
if (!SELF.has(word.toLowerCase()) && !subject) subject = word;
|
||||
named = true;
|
||||
};
|
||||
for (const phrasing of PHRASINGS) take(phrasing.side, phrasing.pattern.exec(prompt)?.[1]);
|
||||
const which = WHICH_CALLS.exec(prompt);
|
||||
if (which) take(which[1].toLowerCase() === 'welche' && /(?:en|[^s]s)$/iu.test(which[2]) ? 'outgoing' : 'incoming', which[3]);
|
||||
if (!named) for (const general of GENERAL) if (general.pattern.test(prompt)) sides.add(general.side);
|
||||
if (!sides.size) return undefined;
|
||||
return { sides: (['incoming', 'outgoing'] as const).filter(side => sides.has(side)), language: GERMAN.test(prompt) ? 'de' : 'en',
|
||||
...subject ? { subject } : {} };
|
||||
}
|
||||
|
||||
/** A question about another symbol is not about this selection. Without a written
|
||||
* subject, any symbol-shaped word in the question still counts as naming one. */
|
||||
function aboutSelection(prompt: string, question: RelationshipQuestion, evidence: GalaxyEvidence): boolean {
|
||||
const selected = new Set([evidence.label, evidence.label.split('/').pop()!, ...evidence.roots.map(root => root.name)].map(name => name.toLowerCase()));
|
||||
const matches = (name: string) => selected.has(name.toLowerCase()) || selected.has(name.split(/[.:]/).pop()!.toLowerCase());
|
||||
if (question.subject) return matches(question.subject);
|
||||
const snake = prompt.match(/\b[A-Za-z]\w*_\w+\b/g) ?? [];
|
||||
const named = [...mentionsIn(prompt), ...quotedNamesIn(prompt), ...identifierNamesIn(prompt), ...snake];
|
||||
return !named.length || named.some(matches);
|
||||
}
|
||||
|
||||
const quote = (value: string) => `\`${value.replace(/`/g, "'")}\``;
|
||||
@@ -54,13 +119,13 @@ function listed(question: RelationshipQuestion, evidence: GalaxyEvidence): strin
|
||||
}
|
||||
|
||||
/** A complete, deterministic answer from the current Galaxy evidence, or undefined
|
||||
* when the question is not about callers or callees of the selection. */
|
||||
* when the question is not a list of callers or callees of the selection. */
|
||||
export function relationshipAnswer(prompt: string, contexts: readonly BrowserChatContext[]): { markdown: string; context: BrowserChatContext } | undefined {
|
||||
const question = relationshipQuestion(prompt);
|
||||
if (!question) return undefined;
|
||||
for (const context of contexts) {
|
||||
const evidence = readGalaxyEvidence(context.text);
|
||||
if (evidence && aboutSelection(prompt, evidence)) return { markdown: listed(question, evidence), context };
|
||||
if (evidence && aboutSelection(prompt, question, evidence)) return { markdown: listed(question, evidence), context };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ export const relationshipWords: { en: RelationshipWords; de: RelationshipWords }
|
||||
/** Notes of the local chat dock about how an answer was produced or bounded. */
|
||||
export const browserChatText = {
|
||||
shortened: 'Shortened (token limit)',
|
||||
/** Offered under a listed answer, which the model did not write. */
|
||||
askModel: 'Ask the model',
|
||||
capacity: (nodes: number, edges: number, model: string, shown: number) =>
|
||||
`${nodes} nodes / ${edges} edges: too large for the local ${model} model; showing ${shown}`,
|
||||
historyTrimmed: (count: number) => `${count} earlier ${count === 1 ? 'message was' : 'messages were'} left out to fit the input limit.`,
|
||||
|
||||
Reference in New Issue
Block a user