Skip to content

Commit d9771b4

Browse files
committed
refactor: code clean up, prep for review and release
1 parent d295eaf commit d9771b4

27 files changed

+779
-525
lines changed

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,11 @@
141141
"pretty-bytes": "^5.6.0",
142142
"ps-node": "^0.1.6",
143143
"read-pkg-up": "^7.0.1",
144+
"reflect-metadata": "^0.2.2",
144145
"semver": "^7.3.5",
145146
"sql-formatter": "^4.0.2",
146147
"strip-ansi": "^6.0.0",
148+
"tsyringe": "^4.8.0",
147149
"validator": "^13.7.0",
148150
"w3c-xmlserializer": "^2.0.0",
149151
"yargs": "^17.1.1"

packages/cli/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
/* eslint-disable func-names */
77
/* eslint-disable max-classes-per-file */
88

9+
import 'reflect-metadata';
10+
911
const yargs = require('yargs');
1012
const { promises: fsp, readFileSync } = require('fs');
1113
const { queue } = require('async');

packages/cli/src/cmds/index/rpc.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ import { navieMetadataV1, navieMetadataV2 } from '../../rpc/navie/metadata';
2929
import { navieSuggestHandlerV1 } from '../../rpc/navie/suggest';
3030
import { navieWelcomeV2 } from '../../rpc/navie/welcome';
3131
import { navieRegisterV1 } from '../../rpc/navie/register';
32-
import { navieThreadSendMessageHandler } from '../../rpc/navie/thread/sendMessage';
32+
import { navieThreadSendMessageHandler } from '../../rpc/navie/thread/handlers/sendMessage';
3333
import {
3434
navieThreadPinItemHandler,
3535
navieThreadUnpinItemHandler,
36-
} from '../../rpc/navie/thread/pinItem';
37-
import { navieThreadQueryHandler } from '../../rpc/navie/thread/query';
36+
} from '../../rpc/navie/thread/handlers/pinItem';
37+
import { navieThreadQueryHandler } from '../../rpc/navie/thread/handlers/query';
3838
import NavieService from '../../rpc/navie/services/navieService';
39+
import { ThreadIndexService } from '../../rpc/navie/services/threadIndexService';
40+
import { container } from 'tsyringe';
41+
import ThreadService from '../../rpc/navie/services/threadService';
3942

4043
export const command = 'rpc';
4144
export const describe = 'Run AppMap JSON-RPC server';
@@ -58,6 +61,8 @@ type HandlerArguments = yargs.ArgumentsCamelCase<
5861
>;
5962

6063
export function rpcMethods(navie: INavieProvider, codeEditor?: string): RpcHandler<any, any>[] {
64+
const threadService = container.resolve(ThreadService);
65+
const threadIndexService = container.resolve(ThreadIndexService);
6166
return [
6267
search(),
6368
appmapStatsV1(),
@@ -77,19 +82,21 @@ export function rpcMethods(navie: INavieProvider, codeEditor?: string): RpcHandl
7782
navieMetadataV2(),
7883
navieSuggestHandlerV1(navie),
7984
navieWelcomeV2(navie),
80-
navieRegisterV1(codeEditor),
81-
navieThreadSendMessageHandler(),
82-
navieThreadPinItemHandler(),
83-
navieThreadUnpinItemHandler(),
84-
navieThreadQueryHandler(),
85+
navieRegisterV1(threadService, codeEditor),
86+
navieThreadSendMessageHandler(threadService),
87+
navieThreadPinItemHandler(threadService),
88+
navieThreadUnpinItemHandler(threadService),
89+
navieThreadQueryHandler(threadIndexService),
8590
];
8691
}
8792

8893
export const handler = async (argv: HandlerArguments) => {
8994
verbose(argv.verbose);
9095

9196
const navie = buildNavieProvider(argv);
92-
NavieService.registerNavieProvider(navie);
97+
98+
ThreadIndexService.useDefault();
99+
NavieService.bindNavieProvider(navie);
93100

94101
let codeEditor: string | undefined = argv.codeEditor;
95102
if (!codeEditor) {

packages/cli/src/cmds/index/rpcServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Server } from 'http';
1111

1212
import shadowLocalhost from '../../lib/shadowLocalhost';
1313
import { RpcCallback, RpcHandler, toJaysonRpcError } from '../../rpc/rpc';
14-
import { sseMiddleware } from '../../rpc/navie/thread/subscribe';
14+
import { sseMiddleware } from '../../rpc/navie/thread/middleware';
1515

1616
const debug = makeDebug('appmap:rpcServer');
1717

@@ -64,7 +64,7 @@ export default class RPCServer {
6464
const app = connect();
6565
app.use(cors({ methods: ['POST'] }));
6666
app.use(jsonParser());
67-
app.use(sseMiddleware);
67+
app.use(sseMiddleware());
6868
app.use(server.middleware());
6969
const listener = app.listen(this.bindPort, 'localhost');
7070

packages/cli/src/rpc/explain/navie/navie-local.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {
1616
import reportFetchError from './report-fetch-error';
1717
import assert from 'assert';
1818
import Trajectory from './trajectory';
19-
import { getThread } from '../../navie/thread';
2019
import { verbose } from '../../../utils';
20+
import { container } from 'tsyringe';
21+
import ThreadService from '../../navie/services/threadService';
2122

2223
const OPTION_SETTERS: Record<
2324
string,
@@ -135,7 +136,8 @@ export default class LocalNavie extends EventEmitter implements INavie {
135136
const startTime = Date.now();
136137
let chatHistory: Navie.ChatHistory = [];
137138
try {
138-
const thread = await getThread(threadId);
139+
const threadService = container.resolve(ThreadService);
140+
const thread = await threadService.getThread(threadId);
139141
chatHistory = thread.getChatHistory();
140142
} catch (e) {
141143
if (verbose()) {

packages/cli/src/rpc/navie/register.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { RpcHandler } from '../rpc';
1212
import { getLLMConfiguration } from '../llmConfiguration';
1313
import detectAIEnvVar from '../../cmds/index/aiEnvVar';
1414
import configuration from '../configuration';
15-
import { INavieProvider } from '../explain/navie/inavie';
16-
import { registerThread } from './thread';
15+
import ThreadService from './services/threadService';
1716

1817
export async function register(
18+
threadService: ThreadService,
1919
codeEditor: string | undefined
2020
): Promise<NavieRpc.V1.Register.Response> {
2121
const modelParameters = {
@@ -42,18 +42,19 @@ export async function register(
4242
if (codeEditor) projectParameters.codeEditor = codeEditor;
4343

4444
const thread = await AI.createConversationThread({ modelParameters, projectParameters });
45-
registerThread(thread);
45+
threadService.registerThread(thread);
4646

4747
return { thread };
4848
}
4949

5050
export function navieRegisterV1(
51+
threadService: ThreadService,
5152
codeEditor?: string
5253
): RpcHandler<NavieRpc.V1.Register.Params, NavieRpc.V1.Register.Response> {
5354
return {
5455
name: NavieRpc.V1.Register.Method,
5556
handler: async () => {
56-
return register(codeEditor);
57+
return register(threadService, codeEditor);
5758
},
5859
};
5960
}

packages/cli/src/rpc/navie/services/contextService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import collectProjectInfos from '../../../cmds/navie/projectInfo';
55
import collectHelp from '../../../cmds/navie/help';
66
import detectCodeEditor from '../../../lib/detectCodeEditor';
77
import configuration from '../../configuration';
8+
import { autoInjectable } from 'tsyringe';
89

10+
@autoInjectable()
911
export class ContextService {
1012
private readonly codeEditor?: string;
1113

packages/cli/src/rpc/navie/services/navieService.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import INavie, { INavieProvider } from '../../explain/navie/inavie';
33
import { ContextService } from './contextService';
44
import { randomUUID } from 'node:crypto';
55
import EventEmitter from 'node:events';
6+
import { autoInjectable, container, inject } from 'tsyringe';
67

78
interface ContextEvent<Type, Req, Res> {
89
type: Type;
@@ -44,22 +45,27 @@ function contextEvent<
4445
};
4546
}
4647

48+
@autoInjectable()
4749
export default class NavieService {
48-
private static readonly contextService = new ContextService();
49-
private static navieProvider?: INavieProvider;
50+
private static NAVIE_PROVIDER = 'INavieProvider';
5051

51-
static registerNavieProvider(navieProvider: INavieProvider): void {
52-
NavieService.navieProvider = navieProvider;
52+
constructor(
53+
@inject(ContextService) private readonly contextService: ContextService,
54+
@inject(NavieService.NAVIE_PROVIDER) private readonly navieProvider?: INavieProvider
55+
) {}
56+
57+
static bindNavieProvider(navieProvider?: INavieProvider) {
58+
container.registerInstance(this.NAVIE_PROVIDER, navieProvider);
5359
}
5460

55-
static getNavieProvider(): INavieProvider {
61+
getNavieProvider(): INavieProvider {
5662
if (!this.navieProvider) {
5763
throw new Error('No navie provider available');
5864
}
5965
return this.navieProvider;
6066
}
6167

62-
static getNavie(_navieProvider?: INavieProvider): [INavie, ContextEmitter] {
68+
getNavie(_navieProvider?: INavieProvider): [INavie, ContextEmitter] {
6369
const navieProvider = _navieProvider ?? this.getNavieProvider();
6470
const contextEmitter = new EventEmitter();
6571
const navie = navieProvider(
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import sqlite3 from 'better-sqlite3';
2+
import { homedir } from 'node:os';
3+
import { join } from 'node:path';
4+
import configuration from '../../configuration';
5+
import { container, inject, injectable, singleton } from 'tsyringe';
6+
7+
const INITIALIZE_SQL = `CREATE TABLE IF NOT EXISTS threads (
8+
id INTEGER PRIMARY KEY AUTOINCREMENT,
9+
uuid TEXT NOT NULL UNIQUE,
10+
path TEXT NOT NULL,
11+
title TEXT,
12+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
13+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
14+
CONSTRAINT uuid_format CHECK (length(uuid) = 36)
15+
);
16+
17+
CREATE INDEX IF NOT EXISTS idx_created_at ON threads (created_at);
18+
CREATE INDEX IF NOT EXISTS idx_uuid ON threads (uuid);
19+
20+
CREATE TABLE IF NOT EXISTS project_directories (
21+
id INTEGER PRIMARY KEY AUTOINCREMENT,
22+
path TEXT NOT NULL,
23+
thread_id TEXT NOT NULL,
24+
FOREIGN KEY(thread_id) REFERENCES threads(uuid),
25+
UNIQUE (thread_id, path)
26+
);
27+
28+
CREATE INDEX IF NOT EXISTS idx_thread_id ON project_directories (thread_id);
29+
`;
30+
31+
const QUERY_INSERT_THREAD_SQL = `INSERT INTO threads (uuid, path, title) VALUES (?, ?, ?)
32+
ON CONFLICT (uuid) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, title = ?`;
33+
const QUERY_DELETE_THREAD_SQL = `DELETE FROM threads WHERE uuid = ?`;
34+
const QUERY_INSERT_PROJECT_DIRECTORY_SQL = `INSERT INTO project_directories (thread_id, path) VALUES (?, ?)
35+
ON CONFLICT (thread_id, path) DO NOTHING`;
36+
37+
interface QueryOptions {
38+
uuid?: string;
39+
maxCreatedAt?: Date;
40+
orderBy?: 'created_at' | 'updated_at';
41+
limit?: number;
42+
offset?: number;
43+
projectDirectories?: string[];
44+
}
45+
46+
interface ThreadIndexItem {
47+
id: string;
48+
path: string;
49+
title: string;
50+
createdAt: Date;
51+
updatedAt: Date;
52+
}
53+
54+
/**
55+
* A service for managing the conversation thread index. The thread index is a SQLite database that
56+
* stores information about threads, such as their path and title.
57+
*/
58+
@singleton()
59+
@injectable()
60+
export class ThreadIndexService {
61+
private queryInsert: sqlite3.Statement;
62+
private queryDelete: sqlite3.Statement;
63+
private queryInsertProjectDirectory: sqlite3.Statement;
64+
65+
static readonly DEFAULT_DATABASE_PATH = join(homedir(), '.appmap', 'navie', 'thread-index.db');
66+
static readonly DATABASE = 'ThreadIndexDatabase';
67+
68+
constructor(@inject(ThreadIndexService.DATABASE) private readonly db: sqlite3.Database) {
69+
this.db.exec(INITIALIZE_SQL);
70+
71+
this.queryInsert = this.db.prepare(QUERY_INSERT_THREAD_SQL);
72+
this.queryDelete = this.db.prepare(QUERY_DELETE_THREAD_SQL);
73+
this.queryInsertProjectDirectory = this.db.prepare(QUERY_INSERT_PROJECT_DIRECTORY_SQL);
74+
}
75+
76+
/**
77+
* Binds the database to a sqlite3 instance on disk at the default database path
78+
*/
79+
static useDefault() {
80+
const db = new sqlite3(this.DEFAULT_DATABASE_PATH);
81+
container.registerInstance(this.DATABASE, db);
82+
}
83+
84+
/**
85+
* Indexes a thread with the given path and title. If the thread is already indexed, it will be
86+
* updated.
87+
*
88+
* @param threadId The thread ID
89+
* @param path The path to the thread
90+
* @param title The title of the thread
91+
*/
92+
index(threadId: string, path: string, title?: string) {
93+
const { projectDirectories } = configuration();
94+
this.db.transaction(() => {
95+
this.queryInsert.run(threadId, path, title, title);
96+
97+
// Project directories are written on every index update.
98+
//
99+
// This is likely not necessary but it'll handle the edge case where an additional project
100+
// directory is added mid-way through a conversation.
101+
projectDirectories.forEach((projectDirectory) => {
102+
this.queryInsertProjectDirectory.run(threadId, projectDirectory);
103+
});
104+
})();
105+
return;
106+
}
107+
108+
/**
109+
* Deletes a thread from the index.
110+
*
111+
* @param threadId The thread ID
112+
*/
113+
delete(threadId: string) {
114+
return this.queryDelete.run(threadId);
115+
}
116+
117+
/**
118+
* Queries the index for threads.
119+
*
120+
* @param options The options to query with
121+
* @returns The threads that match the query
122+
*/
123+
query(options: QueryOptions): ThreadIndexItem[] {
124+
let queryString = `SELECT uuid as id, threads.path, title, created_at, updated_at FROM threads`;
125+
const params: unknown[] = [];
126+
if (options.uuid) {
127+
queryString += ` WHERE uuid = ?`;
128+
params.push(options.uuid);
129+
}
130+
if (options.maxCreatedAt) {
131+
queryString += ` AND created_at < ?`;
132+
params.push(options.maxCreatedAt);
133+
}
134+
if (options.orderBy) {
135+
queryString += ` ORDER BY ? DESC`;
136+
params.push(options.orderBy);
137+
}
138+
if (options.limit) {
139+
queryString += ` LIMIT ?`;
140+
params.push(options.limit);
141+
}
142+
if (options.offset) {
143+
queryString += ` OFFSET ?`;
144+
params.push(options.offset);
145+
}
146+
if (options.projectDirectories) {
147+
if (options.projectDirectories.length === 0) {
148+
// If `projectDirectories` is an empty array, we want to return all threads that have no
149+
// project directories associated with them. This is edge-casey, but this does occur in
150+
// development.
151+
//
152+
// If you're looking to query for threads with any project directory, leave `projectDirectories`
153+
// as `undefined`.
154+
queryString += ` LEFT JOIN project_directories ON uuid = thread_id WHERE project_directories.path IS NULL`;
155+
} else {
156+
queryString += ` INNER JOIN project_directories ON uuid = thread_id WHERE project_directories.path IN (${options.projectDirectories
157+
.map(() => '?')
158+
.join(',')})`;
159+
params.push(...options.projectDirectories);
160+
}
161+
}
162+
const query = this.db.prepare(queryString);
163+
return query.all(...params) as ThreadIndexItem[];
164+
}
165+
}

0 commit comments

Comments
 (0)