Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions bin/explorbot-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,17 +509,18 @@ program
path: options.path || process.cwd(),
});

const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
const tracker = new KnowledgeTracker();

if (url && description) {
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
const tracker = new KnowledgeTracker();
const result = tracker.addKnowledge(url, description);
const action = result.isNewFile ? 'Created' : 'Updated';
console.log(`Knowledge ${action} in: ${result.filename}`);
return;
}

const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
exitOnCtrlC: false,
patchConsole: false,
});
Expand Down
2 changes: 1 addition & 1 deletion src/ai/captain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class Captain extends CaptainBase implements Agent {
constructor(explorBot: ExplorBot) {
super();
this.explorBot = explorBot;
this.experienceTracker = new ExperienceTracker();
this.experienceTracker = explorBot.experienceTracker();
}

setCommandExecutor(fn: (cmd: string) => Promise<void>, descriptions: { name: string; description: string; options: string }[]): void {
Expand Down
4 changes: 2 additions & 2 deletions src/ai/historian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export class Historian extends HistorianBase {
declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
declare savedFiles: Set<string>;

constructor(provider: Provider, experienceTracker?: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; helper: any }) {
constructor(provider: Provider, experienceTracker: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; helper: any }) {
super();
this.provider = provider;
this.experienceTracker = experienceTracker || new ExperienceTracker();
this.experienceTracker = experienceTracker;
this.reporter = reporter;
this.stateManager = stateManager;
this.config = config;
Expand Down
6 changes: 4 additions & 2 deletions src/ai/historian/codeceptjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { ActionResult } from '../../action-result.ts';
import { ConfigParser } from '../../config.ts';
import { KnowledgeTracker } from '../../knowledge-tracker.ts';
import type { StateManager } from '../../state-manager.ts';
import type { Plan } from '../../test-plan.ts';
import { tag } from '../../utils/logger.ts';
import { relativeToCwd } from '../../utils/next-steps.ts';
Expand All @@ -21,6 +21,7 @@ export interface CodeceptJSMethods {
export function WithCodeceptJS<T extends Constructor>(Base: T) {
return class extends Base {
declare savedFiles: Set<string>;
declare stateManager?: StateManager;

toCode(conversation: Conversation, scenario: string): string {
const toolExecutions = conversation.getToolExecutions();
Expand Down Expand Up @@ -108,7 +109,8 @@ export function WithCodeceptJS<T extends Constructor>(Base: T) {
}

private getKnowledgeLines(url: string, indent = ' '): string[] {
const knowledgeTracker = new KnowledgeTracker();
const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
if (!knowledgeTracker) return [];
const state = new ActionResult({ url });
const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);

Expand Down
6 changes: 4 additions & 2 deletions src/ai/historian/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { ActionResult } from '../../action-result.ts';
import { ConfigParser } from '../../config.ts';
import { KnowledgeTracker } from '../../knowledge-tracker.ts';
import type { StateManager } from '../../state-manager.ts';
import { type PlaywrightRecorder, type TraceCall, renderAssertion, renderCall } from '../../playwright-recorder.ts';
import type { Plan } from '../../test-plan.ts';
import { tag } from '../../utils/logger.ts';
Expand All @@ -25,6 +25,7 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
return class extends Base {
declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
declare savedFiles: Set<string>;
declare stateManager?: StateManager;

async toPlaywrightCode(conversation: Conversation, scenario: string): Promise<string> {
const toolExecutions = conversation.getToolExecutions();
Expand Down Expand Up @@ -146,7 +147,8 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
}

private getPlaywrightKnowledgeLines(url: string, indent = ' '): string[] {
const knowledgeTracker = new KnowledgeTracker();
const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
if (!knowledgeTracker) return [];
const state = new ActionResult({ url });
const { wait, waitForElement } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement']);

Expand Down
6 changes: 3 additions & 3 deletions src/ai/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ class Navigator implements Agent {
`;
private explorer: Explorer;

constructor(explorer: Explorer, provider: Provider, experienceTracker?: ExperienceTracker) {
constructor(explorer: Explorer, provider: Provider) {
this.provider = provider;
this.explorer = explorer;
this.knowledgeTracker = new KnowledgeTracker();
this.experienceTracker = experienceTracker || new ExperienceTracker();
this.knowledgeTracker = explorer.getKnowledgeTracker();
this.experienceTracker = explorer.getStateManager().getExperienceTracker();
this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
}

Expand Down
4 changes: 2 additions & 2 deletions src/commands/compact-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class CompactCommand extends BaseCommand {
const dryRun = !!opts.dryRun;
const merge = opts.merge !== false;

const tracker = this.explorBot.getExperienceTracker();
const tracker = this.explorBot.experienceTracker();
const files = this.resolveTarget(tracker, target);

if (files === null) {
Expand Down Expand Up @@ -73,7 +73,7 @@ export class CompactCommand extends BaseCommand {
private async runFullSweep(compactor: ReturnType<typeof this.explorBot.agentExperienceCompactor>, merge: boolean): Promise<{ merged: number; compacted: number }> {
if (!merge) {
tag('info').log('Compacting all experience files (merge skipped)…');
const compacted = await compactor.compactFiles(this.explorBot.getExperienceTracker().getAllExperience());
const compacted = await compactor.compactFiles(this.explorBot.experienceTracker().getAllExperience());
return { merged: 0, compacted };
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/context-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class ContextCommand extends BaseCommand {

const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual });
const experienceTracker = explorer.getStateManager().getExperienceTracker();
const knowledgeTracker = this.explorBot.getKnowledgeTracker();
const knowledgeTracker = this.explorBot.knowledgeTracker();

let mode: ContextMode = 'compact';
if (opts.full) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/context-knowledge-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class ContextKnowledgeCommand extends BaseCommand {
}

const actionResult = ActionResult.fromState(state);
const knowledgeTracker = this.explorBot.getKnowledgeTracker();
const knowledgeTracker = this.explorBot.knowledgeTracker();
const knowledge = knowledgeTracker.getRelevantKnowledge(actionResult);

if (knowledge.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/experience-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class ExperienceCommand extends BaseCommand {
}
const recency = opts.recent ? 'recent' : opts.old ? 'old' : undefined;

const tracker = this.explorBot.getExperienceTracker();
const tracker = this.explorBot.experienceTracker();
const [first, second] = remaining;

const combinedRef = first?.match(/^([A-Z]+)[.\-]?(\d+)$/i);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/knows-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class KnowsCommand extends BaseCommand {

async execute(args: string, limit = 0): Promise<void> {
const url = args.trim();
const tracker = this.explorBot.getKnowledgeTracker();
const tracker = this.explorBot.knowledgeTracker();

if (!url) {
const allKnowledge = tracker.listAllKnowledge();
Expand Down
4 changes: 3 additions & 1 deletion src/commands/learn-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export class LearnCommand extends BaseCommand {

if (!note) {
const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
const explorer = this.explorBot.getExplorer();
const state = explorer.getStateManager().getCurrentState();
const initialUrl = state?.url || '';

const { unmount } = render(
React.createElement(AddKnowledge, {
initialUrl,
knowledgeTracker: explorer.getKnowledgeTracker(),
onComplete: () => unmount(),
onCancel: () => unmount(),
}),
Expand Down
12 changes: 5 additions & 7 deletions src/components/AddKnowledge.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Box, Text, useInput } from 'ink';
import React, { useEffect, useState } from 'react';
import { KnowledgeTracker } from '../knowledge-tracker.js';
import type { KnowledgeTracker } from '../knowledge-tracker.js';
import InputReadline from './InputReadline.js';

interface AddKnowledgeProps {
initialUrl?: string;
knowledgeTracker: KnowledgeTracker;
onComplete?: () => void;
onCancel?: () => void;
}

const AddKnowledge: React.FC<AddKnowledgeProps> = ({ initialUrl = '', onComplete, onCancel }) => {
const AddKnowledge: React.FC<AddKnowledgeProps> = ({ initialUrl = '', knowledgeTracker, onComplete, onCancel }) => {
const [urlPattern, setUrlPattern] = useState(initialUrl);
const [description, setDescription] = useState('');
const [activeField, setActiveField] = useState<'url' | 'description'>(initialUrl ? 'description' : 'url');
Expand All @@ -20,18 +21,16 @@ const AddKnowledge: React.FC<AddKnowledgeProps> = ({ initialUrl = '', onComplete

useEffect(() => {
try {
const knowledgeTracker = new KnowledgeTracker();
const urls = knowledgeTracker.getExistingUrls();
setSuggestedUrls(urls);
} catch (error) {
console.error('Failed to load suggestions:', error);
}
}, []);
}, [knowledgeTracker]);

useEffect(() => {
if (urlPattern.trim()) {
try {
const knowledgeTracker = new KnowledgeTracker();
const knowledge = knowledgeTracker.getKnowledgeForUrl(urlPattern);
setExistingKnowledge(knowledge);
} catch (error) {
Expand All @@ -41,15 +40,14 @@ const AddKnowledge: React.FC<AddKnowledgeProps> = ({ initialUrl = '', onComplete
} else {
setExistingKnowledge([]);
}
}, [urlPattern]);
}, [urlPattern, knowledgeTracker]);

const handleSave = () => {
if (!urlPattern.trim() || !description.trim()) {
return;
}

try {
const knowledgeTracker = new KnowledgeTracker();
const result = knowledgeTracker.addKnowledge(urlPattern.trim(), description.trim());
const action = result.isNewFile ? 'Created' : 'Updated';
console.log(`\nKnowledge ${action} in: ${result.filename}`);
Expand Down
4 changes: 2 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
if (!mounted) return;
setChecklistData({
config: explorBot.getConfig(),
knowledgeTracker: explorBot.getKnowledgeTracker(),
knowledgeTracker: explorBot.knowledgeTracker(),
});
setShowWelcome(true);
setShowInput(true);
Expand Down Expand Up @@ -334,7 +334,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
{showSessionTimer && <SessionTimer startedAt={sessionStartedAtRef.current} />}
</Box>

{showWelcome && <WelcomeCommands hasKnowledge={explorBot.getKnowledgeTracker().listAllKnowledge().length > 0} />}
{showWelcome && <WelcomeCommands hasKnowledge={explorBot.knowledgeTracker().listAllKnowledge().length > 0} />}
{showInput && <Box height={1} />}
{interruptPrompt && showInput && (
<Box paddingX={1}>
Expand Down
4 changes: 2 additions & 2 deletions src/experience-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ export class ExperienceTracker {
private disabled: boolean;
private knowledgeTracker: KnowledgeTracker;

constructor(options: { disabled?: boolean } = {}) {
constructor(knowledgeTracker: KnowledgeTracker, options: { disabled?: boolean } = {}) {
const configParser = ConfigParser.getInstance();
const config = configParser.getConfig();
const configPath = configParser.getConfigPath();
this.disabled = options.disabled ?? false;
this.knowledgeTracker = new KnowledgeTracker();
this.knowledgeTracker = knowledgeTracker;

// Resolve experience directory relative to the config file location (project root)
if (configPath) {
Expand Down
22 changes: 9 additions & 13 deletions src/explorbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class ExplorBot {
private agents: Record<string, any> = {};
private sessionPlans: Plan[] = [];
private lastReportedTestCount = 0;
private _experienceTracker?: ExperienceTracker;
private _knowledgeTracker?: KnowledgeTracker;

constructor(options: ExplorBotOptions = {}) {
this.options = options;
Expand All @@ -86,7 +88,7 @@ export class ExplorBot {

try {
await this.startProviderOnly();
this.explorer = new Explorer(this.config, this.provider, this.options);
this.explorer = new Explorer(this.config, this.provider, this.options, this.experienceTracker(), this.knowledgeTracker());
await this.explorer.start();
if (!this.options.incognito) {
await this.agentExperienceCompactor().autocompact();
Expand Down Expand Up @@ -131,18 +133,12 @@ export class ExplorBot {
return this.explorer;
}

getKnowledgeTracker(): KnowledgeTracker {
if (this.explorer) {
return this.explorer.getKnowledgeTracker();
}
return new KnowledgeTracker();
knowledgeTracker(): KnowledgeTracker {
return (this._knowledgeTracker ||= new KnowledgeTracker());
}

getExperienceTracker(): ExperienceTracker {
if (this.explorer) {
return this.explorer.getStateManager().getExperienceTracker();
}
return new ExperienceTracker();
experienceTracker(): ExperienceTracker {
return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
}

getConfig(): ExplorbotConfig {
Expand Down Expand Up @@ -176,7 +172,7 @@ export class ExplorBot {

agentNavigator(): Navigator {
return (this.agents.navigator ||= this.createAgent(({ ai, explorer }) => {
return new Navigator(explorer, ai, explorer.getStateManager().getExperienceTracker());
return new Navigator(explorer, ai);
}));
}

Expand Down Expand Up @@ -239,7 +235,7 @@ export class ExplorBot {
}

agentExperienceCompactor(): ExperienceCompactor {
return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.getExperienceTracker()));
return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.experienceTracker()));
}

agentQuartermaster(): Quartermaster | null {
Expand Down
7 changes: 4 additions & 3 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RequestStore } from './api/request-store.ts';
import { XhrCapture } from './api/xhr-capture.ts';
import type { ExplorbotConfig } from './config.js';
import { ConfigParser } from './config.js';
import type { ExperienceTracker } from './experience-tracker.js';
import { KnowledgeTracker } from './knowledge-tracker.js';
import { PlaywrightRecorder } from './playwright-recorder.ts';
import { Reporter } from './reporter.ts';
Expand Down Expand Up @@ -75,13 +76,13 @@ class Explorer {
private testConsoleHandler: ((message: any) => void) | null = null;
private testDialogHandler: ((dialog: any) => void) | null = null;

constructor(config: ExplorbotConfig, aiProvider: AIProvider, options?: { show?: boolean; headless?: boolean; incognito?: boolean; session?: string }) {
constructor(config: ExplorbotConfig, aiProvider: AIProvider, options: { show?: boolean; headless?: boolean; incognito?: boolean; session?: string } | undefined, experienceTracker: ExperienceTracker, knowledgeTracker: KnowledgeTracker) {
this.config = config;
this.aiProvider = aiProvider;
this.options = options;
this.initializeContainer();
this.stateManager = new StateManager({ incognito: this.options?.incognito });
this.knowledgeTracker = new KnowledgeTracker();
this.knowledgeTracker = knowledgeTracker;
this.stateManager = new StateManager(experienceTracker, knowledgeTracker);
this.reporter = new Reporter(config.reporter, this.stateManager);
}

Expand Down
10 changes: 6 additions & 4 deletions src/knowledge-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createDebug } from './utils/logger.js';

const debugLog = createDebug('explorbot:knowledge-tracker');

interface Knowledge {
export interface Knowledge {
filePath: string;
url: string;
content: string;
Expand Down Expand Up @@ -46,9 +46,9 @@ export class KnowledgeTracker {
return;
}

const files = readdirSync(this.knowledgeDir)
.filter((file) => file.endsWith('.md'))
.map((file) => join(this.knowledgeDir, file));
const files = readdirSync(this.knowledgeDir, { recursive: true })
.filter((file) => typeof file === 'string' && file.endsWith('.md'))
.map((file) => join(this.knowledgeDir, file as string));

for (const filePath of files) {
try {
Expand Down Expand Up @@ -127,6 +127,8 @@ export class KnowledgeTracker {
writeFileSync(filePath, fileContent, 'utf8');
}

this.isLoaded = false;

return { filename, filePath, isNewFile };
}

Expand Down
Loading
Loading