Skip to content

Commit 658a232

Browse files
author
Marvin Zhang
committed
feat: enhance AI package with chat import service, add tests, and improve data handling
1 parent 28b8639 commit 658a232

File tree

18 files changed

+754
-107
lines changed

18 files changed

+754
-107
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@
6262
"better-sqlite3": "^11.10.0",
6363
"dotenv": "16.5.0",
6464
"tsx": "^4.0.0"
65-
}
65+
},
66+
"packageManager": "[email protected]+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
6667
}

packages/ai/README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# @codervisor/devlog-ai
22

3-
# @codervisor/devlog-ai
4-
53
AI Chat History Extractor & Docker-based Automation - TypeScript implementation for GitHub Copilot and other AI coding assistants in the devlog ecosystem.
64

75
## Features
@@ -14,6 +12,7 @@ AI Chat History Extractor & Docker-based Automation - TypeScript implementation
1412
- **Multiple Export Formats**: Export to JSON and Markdown
1513
- **Search Functionality**: Search through chat content to find specific conversations
1614
- **Statistics**: View usage statistics and patterns
15+
- **Devlog Integration**: Seamlessly integrates with the devlog core system for enhanced project management
1716

1817
### 🤖 Docker-based Automation (NEW!)
1918

@@ -30,6 +29,8 @@ AI Chat History Extractor & Docker-based Automation - TypeScript implementation
3029
- **ESM Support**: Modern ES modules with proper .js extensions for runtime compatibility
3130
- **Extensible Architecture**: Plugin-based parser system for adding new AI assistants
3231
- **Performance Optimized**: Streaming and batch processing for large datasets
32+
- **Type Safety**: Strict TypeScript with minimal `any` usage and proper error handling
33+
- **Comprehensive Testing**: Full test coverage with vitest
3334

3435
## Installation
3536

@@ -94,11 +95,17 @@ npx @codervisor/devlog-ai automation run --scenarios testing --language python
9495
#### Chat History Analysis
9596

9697
```typescript
97-
import { CopilotParser, JSONExporter, MarkdownExporter } from '@codervisor/devlog-ai';
98+
import {
99+
CopilotParser,
100+
JSONExporter,
101+
MarkdownExporter,
102+
DefaultChatImportService,
103+
ChatHubService,
104+
} from '@codervisor/devlog-ai';
98105

99106
// Parse chat data
100107
const parser = new CopilotParser();
101-
const data = await parser.discoverVSCodeCopilotData();
108+
const data = await parser.discoverChatData();
102109

103110
// Get statistics
104111
const stats = parser.getChatStatistics(data);
@@ -126,6 +133,10 @@ await mdExporter.exportChatData(
126133
},
127134
'report.md',
128135
);
136+
137+
// Import to devlog system
138+
const importService = new DefaultChatImportService(storageProvider);
139+
const progress = await importService.importFromCopilot();
129140
```
130141

131142
#### 🤖 Docker Automation

packages/ai/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "tsc",
1010
"clean": "rimraf build",
1111
"dev": "tsc --watch",
12-
"test": "vitest",
12+
"test": "vitest run",
1313
"test:ui": "vitest --ui",
1414
"test:watch": "vitest --watch"
1515
},
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Tests for Exporters
3+
*/
4+
5+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6+
import { readFile, rm, mkdir } from 'fs/promises';
7+
import { resolve } from 'path';
8+
import { JSONExporter } from '../exporters/json.js';
9+
import { MarkdownExporter } from '../exporters/markdown.js';
10+
import type { ChatStatistics } from '../parsers/index.js';
11+
12+
const TEST_OUTPUT_DIR = resolve(process.cwd(), 'test-output');
13+
14+
describe('JSONExporter', () => {
15+
let exporter: JSONExporter;
16+
17+
beforeEach(async () => {
18+
exporter = new JSONExporter();
19+
await mkdir(TEST_OUTPUT_DIR, { recursive: true });
20+
});
21+
22+
afterEach(async () => {
23+
try {
24+
await rm(TEST_OUTPUT_DIR, { recursive: true });
25+
} catch {
26+
// Ignore cleanup errors
27+
}
28+
});
29+
30+
it('should export data to JSON file', async () => {
31+
const testData = {
32+
test: 'data',
33+
number: 42,
34+
array: [1, 2, 3],
35+
};
36+
37+
const outputPath = resolve(TEST_OUTPUT_DIR, 'test.json');
38+
await exporter.exportData(testData, outputPath);
39+
40+
const fileContent = await readFile(outputPath, 'utf-8');
41+
const parsedData = JSON.parse(fileContent);
42+
43+
expect(parsedData).toEqual(testData);
44+
});
45+
46+
it('should handle Date objects in JSON export', async () => {
47+
const testDate = new Date('2023-01-01T00:00:00.000Z');
48+
const testData = {
49+
timestamp: testDate,
50+
};
51+
52+
const outputPath = resolve(TEST_OUTPUT_DIR, 'test-date.json');
53+
await exporter.exportData(testData, outputPath);
54+
55+
const fileContent = await readFile(outputPath, 'utf-8');
56+
const parsedData = JSON.parse(fileContent);
57+
58+
expect(parsedData.timestamp).toBe('2023-01-01T00:00:00.000Z');
59+
});
60+
});
61+
62+
describe('MarkdownExporter', () => {
63+
let exporter: MarkdownExporter;
64+
65+
beforeEach(async () => {
66+
exporter = new MarkdownExporter();
67+
await mkdir(TEST_OUTPUT_DIR, { recursive: true });
68+
});
69+
70+
afterEach(async () => {
71+
try {
72+
await rm(TEST_OUTPUT_DIR, { recursive: true });
73+
} catch {
74+
// Ignore cleanup errors
75+
}
76+
});
77+
78+
it('should export statistics to Markdown', async () => {
79+
const stats: ChatStatistics = {
80+
total_sessions: 2,
81+
total_messages: 5,
82+
message_types: { user: 2, assistant: 3 },
83+
session_types: { chat_session: 2 },
84+
workspace_activity: {},
85+
date_range: {
86+
earliest: '2023-01-01T00:00:00.000Z',
87+
latest: '2023-01-02T00:00:00.000Z',
88+
},
89+
agent_activity: { 'GitHub Copilot': 2 },
90+
};
91+
92+
const exportData = { statistics: stats };
93+
const outputPath = resolve(TEST_OUTPUT_DIR, 'stats.md');
94+
95+
await exporter.exportChatData(exportData, outputPath);
96+
97+
const fileContent = await readFile(outputPath, 'utf-8');
98+
99+
expect(fileContent).toContain('# GitHub Copilot Chat History');
100+
expect(fileContent).toContain('**Total Sessions:** 2');
101+
expect(fileContent).toContain('**Total Messages:** 5');
102+
expect(fileContent).toContain('user: 2');
103+
expect(fileContent).toContain('assistant: 3');
104+
});
105+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Tests for AI Models
3+
*/
4+
5+
import { describe, it, expect } from 'vitest';
6+
import { MessageData, ChatSessionData, WorkspaceDataContainer } from '../models/index.js';
7+
8+
describe('MessageData', () => {
9+
it('should create a message with required fields', () => {
10+
const message = new MessageData({
11+
role: 'user',
12+
content: 'Hello, world!',
13+
});
14+
15+
expect(message.role).toBe('user');
16+
expect(message.content).toBe('Hello, world!');
17+
expect(message.timestamp).toBeInstanceOf(Date);
18+
expect(message.metadata).toEqual({});
19+
});
20+
21+
it('should serialize to dict correctly', () => {
22+
const message = new MessageData({
23+
id: 'msg-1',
24+
role: 'assistant',
25+
content: 'Hello back!',
26+
timestamp: new Date('2023-01-01T00:00:00.000Z'),
27+
metadata: { type: 'assistant_response' },
28+
});
29+
30+
const dict = message.toDict();
31+
32+
expect(dict).toEqual({
33+
id: 'msg-1',
34+
role: 'assistant',
35+
content: 'Hello back!',
36+
timestamp: '2023-01-01T00:00:00.000Z',
37+
metadata: { type: 'assistant_response' },
38+
});
39+
});
40+
41+
it('should deserialize from dict correctly', () => {
42+
const dict = {
43+
id: 'msg-1',
44+
role: 'user',
45+
content: 'Test message',
46+
timestamp: '2023-01-01T00:00:00.000Z',
47+
metadata: { type: 'user_request' },
48+
};
49+
50+
const message = MessageData.fromDict(dict);
51+
52+
expect(message.id).toBe('msg-1');
53+
expect(message.role).toBe('user');
54+
expect(message.content).toBe('Test message');
55+
expect(message.timestamp).toEqual(new Date('2023-01-01T00:00:00.000Z'));
56+
expect(message.metadata).toEqual({ type: 'user_request' });
57+
});
58+
});
59+
60+
describe('ChatSessionData', () => {
61+
it('should create a session with required fields', () => {
62+
const session = new ChatSessionData({
63+
agent: 'GitHub Copilot',
64+
});
65+
66+
expect(session.agent).toBe('GitHub Copilot');
67+
expect(session.timestamp).toBeInstanceOf(Date);
68+
expect(session.messages).toEqual([]);
69+
expect(session.metadata).toEqual({});
70+
});
71+
72+
it('should handle messages correctly', () => {
73+
const messages = [
74+
new MessageData({ role: 'user', content: 'Hello' }),
75+
new MessageData({ role: 'assistant', content: 'Hi there!' }),
76+
];
77+
78+
const session = new ChatSessionData({
79+
agent: 'GitHub Copilot',
80+
messages,
81+
session_id: 'session-1',
82+
});
83+
84+
expect(session.messages).toHaveLength(2);
85+
expect(session.session_id).toBe('session-1');
86+
});
87+
});
88+
89+
describe('WorkspaceDataContainer', () => {
90+
it('should create workspace data with required fields', () => {
91+
const workspace = new WorkspaceDataContainer({
92+
agent: 'GitHub Copilot',
93+
});
94+
95+
expect(workspace.agent).toBe('GitHub Copilot');
96+
expect(workspace.chat_sessions).toEqual([]);
97+
expect(workspace.metadata).toEqual({});
98+
});
99+
100+
it('should handle chat sessions correctly', () => {
101+
const sessions = [
102+
new ChatSessionData({ agent: 'GitHub Copilot', session_id: 'session-1' }),
103+
new ChatSessionData({ agent: 'GitHub Copilot', session_id: 'session-2' }),
104+
];
105+
106+
const workspace = new WorkspaceDataContainer({
107+
agent: 'GitHub Copilot',
108+
chat_sessions: sessions,
109+
workspace_path: '/test/workspace',
110+
});
111+
112+
expect(workspace.chat_sessions).toHaveLength(2);
113+
expect(workspace.workspace_path).toBe('/test/workspace');
114+
});
115+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Tests for Services
3+
*/
4+
5+
import { describe, it, expect, vi, beforeEach } from 'vitest';
6+
import { DefaultChatImportService } from '../services/chat-import-service.js';
7+
import { ChatHubService } from '../services/chat-hub-service.js';
8+
import type { StorageProvider } from '@codervisor/devlog-core';
9+
10+
// Mock storage provider
11+
const mockStorageProvider: StorageProvider = {
12+
saveChatSession: vi.fn(),
13+
saveChatMessages: vi.fn(),
14+
saveChatWorkspace: vi.fn(),
15+
} as any;
16+
17+
describe('DefaultChatImportService', () => {
18+
let service: DefaultChatImportService;
19+
20+
beforeEach(() => {
21+
service = new DefaultChatImportService(mockStorageProvider);
22+
vi.clearAllMocks();
23+
});
24+
25+
it('should create service with storage provider', () => {
26+
expect(service).toBeInstanceOf(DefaultChatImportService);
27+
});
28+
29+
it('should throw error for unsupported source', async () => {
30+
await expect(service.importFromSource('manual' as any)).rejects.toThrow(
31+
'Unsupported chat source: manual',
32+
);
33+
});
34+
});
35+
36+
describe('ChatHubService', () => {
37+
let service: ChatHubService;
38+
39+
beforeEach(() => {
40+
service = new ChatHubService(mockStorageProvider);
41+
vi.clearAllMocks();
42+
});
43+
44+
it('should create service with storage provider', () => {
45+
expect(service).toBeInstanceOf(ChatHubService);
46+
});
47+
48+
it('should ingest empty chat sessions', async () => {
49+
const progress = await service.ingestChatSessions([]);
50+
51+
expect(progress.status).toBe('completed');
52+
expect(progress.progress.totalSessions).toBe(0);
53+
expect(progress.progress.processedSessions).toBe(0);
54+
expect(progress.progress.percentage).toBe(0);
55+
});
56+
57+
it('should return null for non-existent import progress', async () => {
58+
const result = await service.getImportProgress('non-existent');
59+
expect(result).toBeNull();
60+
});
61+
});

packages/ai/src/automation/capture/real-time-parser.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@
77
import { EventEmitter } from 'events';
88
import type { CopilotInteraction } from '../types/index.js';
99

10+
interface TelemetryData {
11+
timestamp?: number;
12+
trigger?: string;
13+
fileName?: string;
14+
fileContent?: string;
15+
line?: number;
16+
character?: number;
17+
precedingText?: string;
18+
followingText?: string;
19+
suggestion?: {
20+
text: string;
21+
confidence: number;
22+
accepted?: boolean;
23+
alternatives?: string[];
24+
};
25+
accepted?: boolean;
26+
responseTime?: number;
27+
metadata?: Record<string, unknown>;
28+
[key: string]: unknown;
29+
}
30+
1031
export class RealTimeCaptureParser extends EventEmitter {
1132
private isCapturing = false;
1233
private interactions: CopilotInteraction[] = [];
@@ -58,10 +79,10 @@ export class RealTimeCaptureParser extends EventEmitter {
5879
/**
5980
* Create interaction from VS Code telemetry data
6081
*/
61-
createInteractionFromTelemetry(telemetryData: any): CopilotInteraction {
82+
createInteractionFromTelemetry(telemetryData: TelemetryData): CopilotInteraction {
6283
return {
6384
timestamp: new Date(telemetryData.timestamp || Date.now()),
64-
trigger: this.mapTriggerType(telemetryData.trigger),
85+
trigger: this.mapTriggerType(telemetryData.trigger || 'unknown'),
6586
context: {
6687
fileName: telemetryData.fileName || 'unknown',
6788
fileContent: telemetryData.fileContent || '',

0 commit comments

Comments
 (0)