Skip to content
Merged
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
112 changes: 79 additions & 33 deletions src/gh-agent/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@ import { fetchMessagesByParentId } from "../lark-agent/agent.ts";
import { createIssue } from "../utils/gh-api.ts";
import { CreateIssueParameters } from "../types/gh-api.ts";
import { issueSchema } from "../schemas/issue-schema.ts";
import Handlebars from "npm:handlebars";
import { format } from "npm:date-fns";
import { zhCN } from "npm:date-fns/locale";
import {
FunctionRegistry,
GPT3Tokenizer,
Message,
Prompt,
PromptFunctions,
PromptMemory,
PromptSectionBase,
RenderedPromptSection,
SystemMessage,
Tokenizer,
UserMessage,
VolatileMemory,
} from "npm:promptrix";

// Get configuration from environment variables
const OPENAI_API_ENDPOINT = Deno.env.get("OPENAI_API_ENDPOINT");
Expand All @@ -13,6 +26,43 @@ const USER_ID = Deno.env.get("USER_ID");
const apiPath = "/api/ai-proxy/openai/v1/chat/completions";
const MAX_RETRIES = 3;

class IssuePromptSection extends PromptSectionBase {
private templateContent: string;
private description: string;
private conversation: string[];

constructor(
templateContent: string,
description: string,
conversation: string[],
) {
super(-1);
this.templateContent = templateContent;
this.description = description;
this.conversation = conversation;
}

renderAsMessages(
_memory: PromptMemory,
_functions: PromptFunctions,
tokenizer: Tokenizer,
_maxTokens: number,
): Promise<RenderedPromptSection<Message<string>[]>> {
const content = this.templateContent
.replace("{{{description}}}", this.description)
.replace("{{{conversation}}}", this.conversation.join("\n"));

return Promise.resolve({
output: [{
role: "user",
content: content,
}],
length: tokenizer.encode(content).length,
tooLong: false,
});
}
}

export async function handleIssueCreateRequest(
parentId: string,
description: string,
Expand All @@ -37,14 +87,20 @@ export function generatePrompt(
description: string,
) {
const templateContent = Deno.readTextFileSync(
new URL("../prompt/issue-create.prompt.md", import.meta.url),
new URL("../prompts/issue-create.prompt.md", import.meta.url),
);

const template = Handlebars.compile(templateContent);
return template({
description,
conversation: descriptionMessages.join("\n"),
});
const prompt = new Prompt([
new SystemMessage(
"You are a helpful assistant that creates GitHub issues from conversations. You must respond with valid JSON format that matches the schema requirements.",
),
new IssuePromptSection(templateContent, description, descriptionMessages),
new UserMessage(
"Please create a GitHub issue based on the above information.",
),
]);

return prompt;
}

export function validateAndParseIssue(
Expand All @@ -69,51 +125,41 @@ export async function generateIssueWithAIFallback(
conversation: string[],
description: string,
) {
let lastError: Error | null = null;
const messages: Array<{ role: string; content: string }> = [
{
role: "system",
content:
"You are a helpful assistant that creates GitHub issues from conversations. You must respond with valid JSON format that matches the schema requirements.",
},
];
const memory = new VolatileMemory({});
const functions = new FunctionRegistry();
const tokenizer = new GPT3Tokenizer();
const maxTokens = Number.MAX_SAFE_INTEGER;

for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const prompt = generatePrompt(conversation, description);
messages.push({ role: "user", content: prompt });

if (lastError) {
messages.push({
role: "assistant",
content: "Previous attempt failed. Here's what went wrong: " +
lastError.message,
});
}
const result = await prompt.renderAsMessages(
memory,
functions,
tokenizer,
maxTokens,
);

const messages = result.output.map((msg) => ({
role: msg.role,
content: msg.content || "",
}));

const response = await callOpenAI(messages);
const issue = validateAndParseIssue(response);
return issue;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
console.error(`Attempt ${attempt} failed:`, error);
lastError = error;

if (attempt < MAX_RETRIES) {
messages.push({
role: "system",
content:
`Attempt ${attempt} failed: ${error.message}\nPlease ensure the response is a valid JSON object with required fields (owner, repo, title, body) and follows the schema. Try again.`,
});
memory.set("lastError", error.message);
}
}
}

console.error("All retry attempts failed, using fallback");

// Fallback: Create a default issue with the original conversation and timestamp
const timestamp = format(new Date(), "MM-dd", { locale: zhCN });

return {
owner: "babelcloud",
repo: "babel-agent",
Expand Down
Loading