Refactor Gemini and StreamingService to enhance streamRequest method, improve error handling in parseStreamChunk, and update service registration for StreamingService.

This commit is contained in:
Andrew Beal 2025-10-05 18:17:47 +01:00
parent 82ee600979
commit 4818741a77
4 changed files with 43 additions and 43 deletions

View file

@ -5,6 +5,7 @@ import type { IPrompt } from "AIClasses/IPrompt";
import { StreamingService, type StreamChunk } from "Services/StreamingService";
import type { Conversation } from "Conversations/Conversation";
import { Role } from "Enums/Role";
import { AIProviderURL } from "Enums/ApiProvider";
export class Gemini implements IAIClass {
private readonly apiKey: string;
@ -14,7 +15,7 @@ export class Gemini implements IAIClass {
public constructor(apiKey: string) {
this.apiKey = apiKey;
this.aiPrompt = Resolve(Services.IPrompt);
this.streamingService = new StreamingService();
this.streamingService = Resolve(Services.StreamingService);
}
public async* streamRequest(conversation: Conversation): AsyncGenerator<StreamChunk, void, unknown> {
@ -48,6 +49,38 @@ export class Gemini implements IAIClass {
]
};
yield* this.streamingService.streamGeminiRequest(this.apiKey, requestBody);
yield* this.streamingService.streamRequest(
AIProviderURL.Gemini.replace("API_KEY", this.apiKey),
requestBody,
this.parseStreamChunk
);
}
private parseStreamChunk(chunk: string): StreamChunk {
try {
const data = JSON.parse(chunk);
let text = "";
const candidate = data.candidates?.[0];
if (candidate) {
if (candidate.content?.parts?.[0]?.text) {
text = candidate.content.parts[0].text;
} else if (candidate.text) {
text = candidate.text;
}
}
const isComplete = !!candidate?.finishReason;
return {
content: text,
isComplete: isComplete,
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown parsing error";
console.error("Failed to parse stream chunk:", message, "Chunk:", chunk);
return { content: "", isComplete: false, error: `Failed to parse chunk: ${message}` };
}
}
}

View file

@ -13,6 +13,7 @@ import { ConversationFileSystemService } from "./ConversationFileSystemService";
import { ConversationHistoryModal } from "Modals/ConversationHistoryModal";
import type { App } from "obsidian";
import { AIFunctionService } from "./AIFunctionService";
import { StreamingService } from "./StreamingService";
export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton(Services.MessageService, new MessageService());
@ -23,6 +24,7 @@ export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton<IPrompt>(Services.IPrompt, new AIPrompt());
RegisterSingleton<AIFunctionService>(Services.AIFunctionService, new AIFunctionService());
RegisterSingleton<StreamingService>(Services.StreamingService, new StreamingService());
RegisterTransient<StreamingMarkdownService>(Services.StreamingMarkdownService, () => new StreamingMarkdownService());

View file

@ -12,8 +12,6 @@ export class Services {
// interfaces
static IAIClass = Symbol("IAIClass");
static IPrompt = Symbol("IPrompt");
static IActioner = Symbol("IActioner");
static IActionDefinitions = Symbol("IActionDefinitions");
// modals
static ConversationHistoryModal = Symbol("ConversationHistoryModal");

View file

@ -1,5 +1,3 @@
import { AIProviderURL } from "Enums/ApiProvider";
export interface StreamChunk {
content: string;
isComplete: boolean;
@ -7,17 +5,14 @@ export interface StreamChunk {
}
export class StreamingService {
/**
* Fetches data from Gemini API with streaming support
* Since Obsidian's request() doesn't support streaming, we use native fetch
*/
public async* streamGeminiRequest(
apiKey: string,
requestBody: unknown
public async* streamRequest(
url: string,
requestBody: unknown,
parseStreamChunk: (chunk: string) => StreamChunk
): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await fetch(
AIProviderURL.Gemini.replace("API_KEY", apiKey),
url,
{
method: "POST",
headers: {
@ -51,7 +46,7 @@ export interface StreamChunk {
for (const line of lines) {
if (line.trim().startsWith("data:")) {
const jsonStr = line.trim().substring(5);
const chunk = this.parseStreamChunk(jsonStr);
const chunk = parseStreamChunk(jsonStr);
lastChunkWasComplete = chunk.isComplete;
yield chunk;
}
@ -74,32 +69,4 @@ export interface StreamChunk {
};
}
}
private parseStreamChunk(chunk: string): StreamChunk {
try {
const data = JSON.parse(chunk);
let text = "";
const candidate = data.candidates?.[0];
if (candidate) {
if (candidate.content?.parts?.[0]?.text) {
text = candidate.content.parts[0].text;
} else if (candidate.text) {
text = candidate.text;
}
}
const isComplete = !!candidate?.finishReason;
return {
content: text,
isComplete: isComplete,
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown parsing error";
console.error("Failed to parse stream chunk:", message, "Chunk:", chunk);
return { content: "", isComplete: false, error: `Failed to parse chunk: ${message}` };
}
}
}