Skip to content

Commit d46402e

Browse files
Merge pull request #204 from olasunkanmi-SE/file-upload-feature
File upload feature
2 parents 50f3f86 + 91b1a74 commit d46402e

File tree

17 files changed

+348
-116
lines changed

17 files changed

+348
-116
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@
334334
},
335335
"dependencies": {
336336
"@anthropic-ai/sdk": "^0.20.9",
337+
"@google/genai": "^0.7.0",
337338
"@google/generative-ai": "^0.21.0",
338339
"@googleapis/customsearch": "^3.2.0",
339340
"@libsql/client": "^0.14.0",
@@ -354,4 +355,4 @@
354355
"sinon": "^17.0.1"
355356
},
356357
"license": "MIT"
357-
}
358+
}

src/agents/base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export abstract class BaseAiAgent
2323
};
2424
}
2525

26-
extractQueries(input: string): string[] | undefined {
26+
protected extractQueries(input: string): string[] | undefined {
2727
const startIndex = input.indexOf("Queries: [");
2828
if (startIndex === -1) {
2929
return;
@@ -41,7 +41,7 @@ export abstract class BaseAiAgent
4141
return queriesArray;
4242
}
4343

44-
extractThought(input: string): string | undefined {
44+
protected extractThought(input: string): string | undefined {
4545
const match = RegExp(/Thought:\s*(.*?)\n/).exec(input);
4646
if (match) {
4747
return match[1].trim();

src/agents/file-upload.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import {
2+
CachedContent,
3+
createPartFromUri,
4+
createUserContent,
5+
GenerateContentResponse,
6+
GoogleGenAI,
7+
} from "@google/genai";
8+
import * as path from "path";
9+
import { BaseAiAgent } from "./base";
10+
import { Orchestrator } from "./orchestrator";
11+
12+
export class FileUploadAgent extends BaseAiAgent {
13+
private readonly ai: GoogleGenAI;
14+
protected readonly orchestrator: Orchestrator;
15+
private static readonly PROCESSING_WAIT_TIME_MS = 6000;
16+
private static readonly MAX_CACHE_PAGE_SIZE = 10;
17+
private static readonly CACHE_MODEL = "gemini-1.5-flash-002";
18+
constructor(private readonly apiKey: string) {
19+
super();
20+
this.ai = new GoogleGenAI({ apiKey: this.apiKey });
21+
this.orchestrator = Orchestrator.getInstance();
22+
}
23+
24+
private async uploadFile(filePath: string, displayName: string) {
25+
try {
26+
return await this.ai.files.upload({
27+
file: filePath,
28+
config: { displayName },
29+
});
30+
} catch (error) {
31+
console.error(`Failed to upload file: ${filePath}`, error);
32+
throw new Error(
33+
`File upload failed: ${error instanceof Error ? error.message : String(error)}`,
34+
);
35+
}
36+
}
37+
38+
async uploadAndProcessFile(
39+
filePath: string,
40+
displayName: string,
41+
prompt: string = "Summarize this document",
42+
): Promise<string | undefined> {
43+
let file;
44+
try {
45+
file = (await this.uploadFile(filePath, displayName)) as any;
46+
const processedFile = await this.waitForProcessing(file.name);
47+
48+
const result = await this.generateContentWithFile(processedFile, prompt);
49+
if (result.response) {
50+
this.orchestrator.publish("onResponse", JSON.stringify(result));
51+
}
52+
return result.response;
53+
} catch (error) {
54+
this.logger.info(`Failed to process file: ${file.name}`);
55+
throw new Error(
56+
`File processing pipeline failed: ${error instanceof Error ? error.message : String(error)}`,
57+
);
58+
}
59+
}
60+
61+
private delay(ms: number): Promise<void> {
62+
return new Promise((resolve) => setTimeout(resolve, ms));
63+
}
64+
65+
private async waitForProcessing(fileName: string, maxRetries = 10) {
66+
try {
67+
let getFile = await this.ai.files.get({ name: fileName });
68+
let retries = 0;
69+
while (getFile.state === "PROCESSING" && retries < maxRetries) {
70+
this.logger.info("☕ File upload in progress, grab a cup of coffee");
71+
await this.delay(FileUploadAgent.PROCESSING_WAIT_TIME_MS);
72+
getFile = await this.ai.files.get({ name: fileName });
73+
retries++;
74+
}
75+
if (getFile.state === "FAILED") {
76+
this.logger.info("File processing failed");
77+
}
78+
return getFile;
79+
} catch (error: any) {
80+
console.error("File processing failed.", error);
81+
throw new Error(error.message);
82+
}
83+
}
84+
85+
private async generateContentWithFile(
86+
file: any,
87+
prompt: string,
88+
cacheName?: string,
89+
): Promise<{
90+
response: string | undefined;
91+
fileName: string;
92+
cache: string | undefined;
93+
}> {
94+
try {
95+
const fileContent = createPartFromUri(file.uri, file.mimeType);
96+
let cached = cacheName
97+
? await this.findOrCreateCache(cacheName, fileContent)
98+
: await this.createNewCache(fileContent);
99+
100+
const response = await this.generateContentWithCache(
101+
prompt,
102+
cached.name ?? "",
103+
);
104+
const fileName = file.fsPath ? path.basename(file.fsPath) : "";
105+
106+
return {
107+
response: response.text,
108+
fileName,
109+
cache: cached.name,
110+
};
111+
} catch (error) {
112+
this.logger.error("Failed to generate content with file", error);
113+
return {
114+
response: undefined,
115+
fileName: "",
116+
cache: undefined,
117+
};
118+
}
119+
}
120+
121+
private async findOrCreateCache(
122+
cacheName: string,
123+
fileContent: any,
124+
): Promise<CachedContent> {
125+
try {
126+
return await this.getDocCache(cacheName);
127+
} catch (error) {
128+
return await this.createNewCache(fileContent);
129+
}
130+
}
131+
132+
private async createNewCache(fileContent: any): Promise<CachedContent> {
133+
const cached = await this.ai.caches.create({
134+
model: FileUploadAgent.CACHE_MODEL,
135+
config: {
136+
contents: createUserContent(fileContent),
137+
systemInstruction: "You are an expert analyzing documents",
138+
},
139+
});
140+
this.logger.info("Cache created:", cached);
141+
return cached;
142+
}
143+
144+
private async generateContentWithCache(
145+
prompt: string,
146+
cacheName: string,
147+
): Promise<GenerateContentResponse> {
148+
return await this.ai.models.generateContent({
149+
model: FileUploadAgent.CACHE_MODEL,
150+
contents: prompt,
151+
config: { cachedContent: cacheName },
152+
});
153+
}
154+
155+
private async getDocCache(name: string): Promise<CachedContent> {
156+
try {
157+
const cache = await this.ai.caches.get({ name });
158+
this.logger.info("Cache found:", cache);
159+
return cache;
160+
} catch (error: any) {
161+
throw new Error("Cache not found or error occurred:", error);
162+
}
163+
}
164+
165+
async getCaches(): Promise<CachedContent[]> {
166+
this.logger.info("Retrieving caches");
167+
const caches: CachedContent[] = [];
168+
169+
try {
170+
const pager = await this.ai.caches.list({
171+
config: { pageSize: FileUploadAgent.MAX_CACHE_PAGE_SIZE },
172+
});
173+
174+
let page = pager.page;
175+
176+
do {
177+
caches.push(...page);
178+
if (!pager.hasNextPage()) break;
179+
page = await pager.nextPage();
180+
} while (true);
181+
182+
return caches;
183+
} catch (error) {
184+
console.log("Failed to retrieve caches", error);
185+
throw new Error(
186+
`Failed to retrieve caches: ${error instanceof Error ? error.message : String(error)}`,
187+
);
188+
}
189+
}
190+
}

src/application/constant.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,12 @@ export const PRIORITY_URLS = [
349349
"docs.oracle.com",
350350
"git-scm.com",
351351
];
352+
353+
const FILE_TYPE_PROMPTS: Record<string, string> = {
354+
pdf: "Extract the main information and key points from this PDF document:",
355+
docx: "Summarize the content of this Word document:",
356+
csv: "Analyze this CSV data and provide key insights:",
357+
txt: "Process this text file and extract the relevant information:",
358+
json: "Parse this JSON file and provide a structured analysis:",
359+
default: "Extract all relevant information from this file:",
360+
};

src/commands/event-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export abstract class EventGenerator implements IEventGenerator {
419419
}
420420
} catch (error) {
421421
this.logger.error(
422-
"Error while passing bit response to the webview",
422+
"Error while passing model response to the webview",
423423
error,
424424
);
425425
}

src/emitter/agent-emitter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export class EventEmitter extends BaseEmitter<Record<string, IEventPayload>> {
1212
onSecretChange: vscode.Event<IEventPayload> =
1313
this.createEvent("onSecretChange");
1414
onBootstrap: vscode.Event<IEventPayload> = this.createEvent("onBootstrap");
15+
onFileUpload: vscode.Event<IEventPayload> = this.createEvent("onFileUpload");
16+
onFileProcessedSuccess: vscode.Event<IEventPayload> = this.createEvent(
17+
"onFileProcessSuccess",
18+
);
1519
onActiveworkspaceUpdate: vscode.Event<IEventPayload> = this.createEvent(
1620
"onActiveworkspaceUpdate",
1721
);

src/emitter/interface.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,16 @@
1-
interface IBaseEmitter {
2-
timestamp: string;
3-
}
1+
type AgentEventKeys =
2+
| "onStatus"
3+
| "onError"
4+
| "onUpdate"
5+
| "onQuery"
6+
| "onResponse"
7+
| "onThinking"
8+
| "onSecretChange"
9+
| "onBootstrap"
10+
| "onActiveworkspaceUpdate"
11+
| "onFileUpload";
412

5-
export type Action = "query" | "update" | "status" | "error";
6-
export type EventState =
7-
| "idle"
8-
| "processing"
9-
| "completed"
10-
| "ui-update"
11-
| "error"
12-
| "query";
13-
14-
export interface IAgentEventMap {
15-
onStatus: IEventPayload;
16-
onError: IEventPayload;
17-
onUpdate: IEventPayload;
18-
onQuery: IEventPayload;
19-
onResponse: IEventPayload;
20-
onThinking: IEventPayload;
21-
onSecretChange: IEventPayload;
22-
onBootstrap: IEventPayload;
23-
onActiveworkspaceUpdate: IEventPayload;
24-
}
13+
export type IAgentEventMap = Record<AgentEventKeys, IEventPayload>;
2514

2615
export interface IEventPayload {
2716
type: string;

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export async function activate(context: vscode.ExtensionContext) {
8787
logger.info(`Logged into GitHub as ${session?.account.label}`);
8888
Memory.getInstance();
8989
const fileUpload = new FileManager(context);
90+
// TODO This is broken. Need to Fix
9091
// const index = CodeIndexingService.createInstance();
9192
// Get each of the folders and call the next line for each
9293
// const result = await index.buildFunctionStructureMap();

src/llms/gemini/gemini.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class GeminiLLM
4646
this.logger = new Logger("GeminiLLM");
4747
this.groqLLM = GroqLLM.getInstance({
4848
apiKey: getAPIKey("groq"),
49-
model: "deepseek-r1-distill-qwen-32b",
49+
model: "deepseek-r1-distill-llama-70b",
5050
});
5151
}
5252

0 commit comments

Comments
 (0)