mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
fix(ui): read "test" as a prompt without a question in any open file
Every word of the open file counted as a name the prompt could ask about, so "test" went to the model whenever the file contained the word, and the model echoed it. Greetings and probe words (test, hallo, hi, ok, ja, danke and the like) now never count as names unless one of them is exactly the selection's own name. The hint for such a prompt no longer offers "Ask the model": the model had nothing to answer there. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { jsonbAggEvidence } from './galaxy-evidence.fixture';
|
||||
import { dockHarness, readerOf } from './chat-dock.fixture';
|
||||
|
||||
const dock = dockHarness();
|
||||
const TESTS = 'from django.test import TestCase\n\n\nclass AggregateTests(TestCase):\n def test_values_list(self):\n # test that the list is ok\n self.assertEqual(1, 1)\n';
|
||||
|
||||
/** A prompt that asks nothing gets example questions, also when the open file contains its word (B5). */
|
||||
describe('prompts without a question about an open file (B5)', () => {
|
||||
it('answers "test" locally although the open file contains "test", and offers no model on that hint', async () => {
|
||||
const { props, runtime } = dock.setup();
|
||||
await dock.render({ ...props, selectionScope: 'django-demo:explore', readerContext: readerOf(TESTS, 'tests/postgres_tests/test_aggregates.py') }); await dock.load();
|
||||
for (const prompt of ['test', 'hallo', 'ok']) {
|
||||
await dock.ask(prompt);
|
||||
expect(dock.answerOf(dock.last())).toMatch(/No question was recognized|wurde keine Frage erkannt/);
|
||||
expect(dock.buttonsOf(dock.last())).toEqual([]);
|
||||
}
|
||||
expect(runtime.countTokens).not.toHaveBeenCalled();
|
||||
expect(runtime.chat).not.toHaveBeenCalled();
|
||||
// A real name of the file is a question.
|
||||
await dock.ask('AggregateTests');
|
||||
expect(runtime.chat).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('offers no model under the hint for a Galaxy selection either', async () => {
|
||||
const { props, runtime } = dock.setup();
|
||||
await dock.render({ ...props, proactiveSelection: jsonbAggEvidence() }); await dock.load();
|
||||
await dock.ask('test');
|
||||
expect(dock.answerOf(dock.last())).toContain('What does JSONBAgg do?');
|
||||
expect(dock.buttonsOf(dock.last())).toEqual([]);
|
||||
expect(runtime.chat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1589,15 +1589,13 @@ describe('general questions, prompts without a question and configuration files
|
||||
expect(runtime.chat).not.toHaveBeenCalled();
|
||||
expect(answer()).toContain('No question was recognized in "test". You can ask, for example:');
|
||||
for (const example of ['What does JSONBAgg do?', 'Who calls JSONBAgg?', 'What does JSONBAgg call?']) expect(answer()).toContain(example);
|
||||
expect(buttons()).toEqual(['Ask the model']);
|
||||
// The model only echoed such a prompt, so the hint offers no model (B5).
|
||||
expect(buttons()).toEqual([]);
|
||||
await type('hallo'); await click('Send ↑');
|
||||
expect(answer()).toContain('In "hallo" wurde keine Frage erkannt. Frage zum Beispiel:');
|
||||
expect(answer()).toContain('In "hallo" wurde keine Frage erkannt.');
|
||||
for (const example of ['Was macht JSONBAgg?', 'Wer ruft JSONBAgg auf?']) expect(answer()).toContain(example);
|
||||
expect(buttons()).toEqual(['Modell fragen']);
|
||||
expect(buttons()).toEqual([]);
|
||||
expect(runtime.chat).not.toHaveBeenCalled();
|
||||
await act(async () => button('Modell fragen').click());
|
||||
expect(runtime.chat).toHaveBeenCalledOnce();
|
||||
expect(runtime.chat.mock.calls[0][0].at(-1)!.content).toContain('User question:\nhallo');
|
||||
// A follow-up does not carry the hint as an answer.
|
||||
await render({ ...props, readerContext: reader('repos:\n - repo: x\n', 'file', '.pre-commit-config.yaml') });
|
||||
await type('?'); await click('Send ↑');
|
||||
|
||||
@@ -66,8 +66,9 @@ const cachedInBrowser = (modelId: string) => isBrowserModelCached(getBrowserMode
|
||||
const messageOf = (error: unknown): string => error instanceof Error ? error.message : String(error);
|
||||
/** The names a Galaxy selection goes by: its label and its symbols with their qualified names. */
|
||||
const galaxyNames = (galaxy: GalaxyEvidence): string[] => [galaxy.label, ...galaxy.roots.flatMap(root => [root.name, root.qualifiedName ?? ''])].filter(Boolean);
|
||||
/** Answers the chat gives itself, which "Ask the model" sends to the model as the question they answer. */
|
||||
const ASKABLE = new Set(['graph', 'suggestion', 'grounded', 'file', 'hint']);
|
||||
/** Answers the chat gives itself, which "Ask the model" sends to the model as the question they answer.
|
||||
* Not the hint to a prompt without a question: the model only echoed "test" or "hallo" (B5). */
|
||||
const ASKABLE = new Set(['graph', 'suggestion', 'grounded', 'file']);
|
||||
const sizeLabel = (bytes: number): string => bytes >= 1_000_000_000 ? `${(bytes / 1_000_000_000).toFixed(2)} GB` : `${Math.ceil(bytes / 1_000_000)} MB`;
|
||||
/** Symbol sources read for explanations and questions, newest last. */
|
||||
const SOURCE_CACHE_SIZE = 32;
|
||||
@@ -902,7 +903,7 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = "
|
||||
{turn.status === 'error' && <p className="cbm-chat-turn-error" role="alert">{turn.error}</p>}
|
||||
{index === turns.length - 1 && turn.answeredFrom === 'suggestion' && turn.suggestion && <button type="button" className="cbm-chat-retry"
|
||||
onClick={() => showSuggestedList(turn)}>{replyWords.showList}</button>}
|
||||
{index === turns.length - 1 && turn.status !== 'generating' && turn.answeredFrom !== 'local' && <button type="button" className="cbm-chat-retry" disabled={phase !== 'ready'} onClick={() => { void send(turn); }}>{turn.answeredFrom ? replyWords.askModel : turn.status === 'error' ? 'Retry' : ownWords.askAgain}</button>}
|
||||
{index === turns.length - 1 && turn.status !== 'generating' && (!turn.answeredFrom || ASKABLE.has(turn.answeredFrom)) && <button type="button" className="cbm-chat-retry" disabled={phase !== 'ready'} onClick={() => { void send(turn); }}>{turn.answeredFrom ? replyWords.askModel : turn.status === 'error' ? 'Retry' : ownWords.askAgain}</button>}
|
||||
</div>
|
||||
</article>; })}
|
||||
</div>}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { knownNames, noQuestion } from './question-intent';
|
||||
import { jsonbAggEvidence } from './galaxy-evidence.fixture';
|
||||
import { readGalaxyEvidence } from './galaxy-evidence';
|
||||
|
||||
/** "test" went to the model whenever the open file contained the word, because every word of
|
||||
* the file counted as a name the prompt could ask about (B5). */
|
||||
describe('greetings and probe words are never names of the open file (B5)', () => {
|
||||
const file = 'import pytest\n\ndef test(): ...\n# hallo hi hello hey moin ok ja nein danke\nclass Aggregate: pass\n';
|
||||
|
||||
it('reads "test", "hallo" and the like as prompts without a question, even when the file contains them', () => {
|
||||
const known = knownNames({ names: ['tests/test_aggregates.py'], texts: [file] });
|
||||
for (const prompt of ['test', 'hallo', 'hi', 'hello', 'hey', 'moin', 'ok', 'ja', 'nein', 'danke', '?', 'Test', 'hallo test']) expect(noQuestion(prompt, known), prompt).toBe(true);
|
||||
// Real words of the file still ask about it.
|
||||
expect(noQuestion('Aggregate', known)).toBe(false);
|
||||
expect(noQuestion('pytest', known)).toBe(false);
|
||||
});
|
||||
|
||||
it('counts a probe word when it is exactly the name of the selection', () => {
|
||||
expect(noQuestion('test', knownNames({ names: ['test'], texts: [file] }))).toBe(false);
|
||||
expect(noQuestion('test', knownNames({ names: ['django-demo.tests.test'], texts: [] }))).toBe(false);
|
||||
// A longer name that only contains the word does not.
|
||||
expect(noQuestion('test', knownNames({ names: ['test_jsonb_agg'], texts: [file] }))).toBe(true);
|
||||
const galaxy = readGalaxyEvidence(jsonbAggEvidence().text)!;
|
||||
expect(noQuestion('test', knownNames({ galaxy, names: ['JSONBAgg'], texts: [] }))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -107,14 +107,23 @@ export function noQuestionAnswer(typed: string, language: 'en' | 'de', subject:
|
||||
}
|
||||
|
||||
/** The names a prompt may use for what is at hand: the selection, the symbols around it, the
|
||||
* words of the open file or attached code. A prompt that names one of them is a question. */
|
||||
* words of the open file or attached code. A prompt that names one of them is a question.
|
||||
* A greeting or probe ("test", "hallo", "ok") is none of them, even where the file has the
|
||||
* word, unless it is exactly the selection's own name: "test" went to the model whenever the
|
||||
* open file contained it (B5). */
|
||||
export function knownNames(sources: { galaxy?: GalaxyEvidence; texts: readonly string[]; names: readonly string[] }): (word: string) => boolean {
|
||||
const words = new Set<string>();
|
||||
const add = (text: string) => { for (const word of text.match(/[\p{L}\p{N}_][\p{L}\p{N}_.-]*/gu) ?? []) words.add(word.toLowerCase()); };
|
||||
sources.texts.forEach(add);
|
||||
sources.names.forEach(name => { words.add(name.toLowerCase()); words.add((name.split(/[/.:]/).filter(Boolean).pop() ?? name).toLowerCase()); });
|
||||
const lastPart = (name: string) => (name.split(/[/.:]/).filter(Boolean).pop() ?? name).toLowerCase();
|
||||
sources.names.forEach(name => { words.add(name.toLowerCase()); words.add(lastPart(name)); });
|
||||
const galaxy = sources.galaxy;
|
||||
if (galaxy) for (const group of [...galaxy.relationships.incoming, ...galaxy.relationships.outgoing]) for (const file of group.files) file.symbols.forEach(symbol => words.add(symbol.name.toLowerCase()));
|
||||
const own = sources.names.map(name => name.toLowerCase()).filter(name => name.length >= 4);
|
||||
return word => words.has(word.toLowerCase()) || own.some(name => withinEdits(word.toLowerCase(), name, typoBudget(name)));
|
||||
const ownNames = new Set(sources.names.flatMap(name => [name.toLowerCase(), lastPart(name)]));
|
||||
return word => {
|
||||
const lower = word.toLowerCase();
|
||||
if (GREETINGS.has(lower)) return ownNames.has(lower);
|
||||
return words.has(lower) || own.some(name => withinEdits(lower, name, typoBudget(name)));
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user