Skip to content

Commit 3a10e4c

Browse files
committed
Merge branch 'feature/233-implement-openrouter' of https://github.com/landamessenger/git-board-flow into feature/233-implement-openrouter
2 parents c5c9199 + 42ce658 commit 3a10e4c

10 files changed

Lines changed: 456 additions & 23 deletions

File tree

dist/index.js

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129376,9 +129376,12 @@ class AiRepository {
129376129376
(0, logger_1.logError)('Missing required AI configuration');
129377129377
return undefined;
129378129378
}
129379+
(0, logger_1.logDebugInfo)(`🔎 Model: ${model}`);
129380+
(0, logger_1.logDebugInfo)(`🔎 API Key: ***`);
129381+
(0, logger_1.logDebugInfo)(`🔎 Provider Routing: ${JSON.stringify(providerRouting, null, 2)}`);
129379129382
const url = `https://openrouter.ai/api/v1/chat/completions`;
129380129383
try {
129381-
(0, logger_1.logDebugInfo)(`Sending prompt to ${model}: ${prompt}`);
129384+
// logDebugInfo(`Sending prompt to ${model}: ${prompt}`);
129382129385
const requestBody = {
129383129386
model: model,
129384129387
messages: [
@@ -129418,6 +129421,18 @@ class AiRepository {
129418129421
return undefined;
129419129422
}
129420129423
};
129424+
this.askJson = async (ai, prompt) => {
129425+
const result = await this.ask(ai, prompt);
129426+
if (!result) {
129427+
return undefined;
129428+
}
129429+
// Clean the response by removing ```json markers if present
129430+
const cleanedResult = result
129431+
.replace(/^```json\n?/, '') // Remove ```json at the start
129432+
.replace(/\n?```$/, '') // Remove ``` at the end
129433+
.trim();
129434+
return JSON.parse(cleanedResult);
129435+
};
129421129436
}
129422129437
}
129423129438
exports.AiRepository = AiRepository;
@@ -130548,6 +130563,70 @@ class FileRepository {
130548130563
/^\/\*\*$/.test(trimmed) ||
130549130564
/^\*\/$/.test(trimmed));
130550130565
};
130566+
this.getFileTree = async (owner, repository, token, branch, ignoreFiles, progress) => {
130567+
const fileContents = await this.getRepositoryContent(owner, repository, token, branch, ignoreFiles, progress);
130568+
// Create root nodes for both trees
130569+
const rootWithContent = {
130570+
name: repository,
130571+
type: 'directory',
130572+
path: '',
130573+
children: []
130574+
};
130575+
const rootWithoutContent = {
130576+
name: repository,
130577+
type: 'directory',
130578+
path: '',
130579+
children: []
130580+
};
130581+
// Process each file path to build both trees
130582+
for (const [filePath, content] of fileContents.entries()) {
130583+
const parts = filePath.split('/');
130584+
let currentLevelWithContent = rootWithContent;
130585+
let currentLevelWithoutContent = rootWithoutContent;
130586+
for (let i = 0; i < parts.length; i++) {
130587+
const part = parts[i];
130588+
const isLastPart = i === parts.length - 1;
130589+
const currentPath = parts.slice(0, i + 1).join('/');
130590+
// Find or create the node in the content tree
130591+
let nodeWithContent = currentLevelWithContent.children?.find(n => n.name === part);
130592+
if (!nodeWithContent) {
130593+
nodeWithContent = {
130594+
name: part,
130595+
type: isLastPart ? 'file' : 'directory',
130596+
path: currentPath,
130597+
children: isLastPart ? undefined : [],
130598+
content: isLastPart ? content : undefined
130599+
};
130600+
if (!currentLevelWithContent.children) {
130601+
currentLevelWithContent.children = [];
130602+
}
130603+
currentLevelWithContent.children.push(nodeWithContent);
130604+
}
130605+
// Find or create the node in the no-content tree
130606+
let nodeWithoutContent = currentLevelWithoutContent.children?.find(n => n.name === part);
130607+
if (!nodeWithoutContent) {
130608+
nodeWithoutContent = {
130609+
name: part,
130610+
type: isLastPart ? 'file' : 'directory',
130611+
path: currentPath,
130612+
children: isLastPart ? undefined : []
130613+
};
130614+
if (!currentLevelWithoutContent.children) {
130615+
currentLevelWithoutContent.children = [];
130616+
}
130617+
currentLevelWithoutContent.children.push(nodeWithoutContent);
130618+
}
130619+
if (!isLastPart) {
130620+
currentLevelWithContent = nodeWithContent;
130621+
currentLevelWithoutContent = nodeWithoutContent;
130622+
}
130623+
}
130624+
}
130625+
return {
130626+
withContent: rootWithContent,
130627+
withoutContent: rootWithoutContent
130628+
};
130629+
};
130551130630
}
130552130631
isMediaOrPdfFile(path) {
130553130632
const mediaExtensions = [
@@ -132176,23 +132255,36 @@ exports.ConfigurationHandler = ConfigurationHandler;
132176132255
Object.defineProperty(exports, "__esModule", ({ value: true }));
132177132256
exports.AskActionUseCase = void 0;
132178132257
const result_1 = __nccwpck_require__(27305);
132258+
const ai_repository_1 = __nccwpck_require__(68307);
132179132259
const docker_repository_1 = __nccwpck_require__(19097);
132180132260
const file_repository_1 = __nccwpck_require__(81503);
132261+
const issue_repository_1 = __nccwpck_require__(40057);
132181132262
const supabase_repository_1 = __nccwpck_require__(79829);
132182132263
const logger_1 = __nccwpck_require__(38836);
132183132264
class AskActionUseCase {
132184132265
constructor() {
132185132266
this.taskId = 'AskActionUseCase';
132186132267
this.dockerRepository = new docker_repository_1.DockerRepository();
132187132268
this.fileRepository = new file_repository_1.FileRepository();
132188-
this.CODE_INSTRUCTION_BLOCK = "Represent the code for semantic search";
132189-
this.CODE_INSTRUCTION_LINE = "Represent each line of code for retrieval";
132269+
this.aiRepository = new ai_repository_1.AiRepository();
132270+
this.issueRepository = new issue_repository_1.IssueRepository();
132190132271
this.CODE_INSTRUCTION_ASK = "Represent the question for retrieving relevant code snippets";
132191132272
}
132192132273
async invoke(param) {
132193132274
(0, logger_1.logInfo)(`Executing ${this.taskId}.`);
132194132275
const results = [];
132195132276
try {
132277+
if (param.ai.getOpenRouterModel().length === 0 || param.ai.getOpenRouterApiKey().length === 0) {
132278+
results.push(new result_1.Result({
132279+
id: this.taskId,
132280+
success: false,
132281+
executed: false,
132282+
errors: [
132283+
`OpenRouter model or API key not found.`,
132284+
],
132285+
}));
132286+
return results;
132287+
}
132196132288
/**
132197132289
* Check if the user from the token is found.
132198132290
*/
@@ -132210,12 +132302,15 @@ class AskActionUseCase {
132210132302
/**
132211132303
* Get the comment body.
132212132304
*/
132305+
let description = '';
132213132306
let commentBody = '';
132214132307
if (param.issue.isIssueComment) {
132215132308
commentBody = param.issue.commentBody;
132309+
description = await this.issueRepository.getDescription(param.owner, param.repo, param.issueNumber, param.tokenUser) ?? '';
132216132310
}
132217132311
else if (param.pullRequest.isPullRequestReviewComment) {
132218132312
commentBody = param.pullRequest.commentBody;
132313+
description = await this.issueRepository.getDescription(param.owner, param.repo, param.issueNumber, param.tokenUser) ?? '';
132219132314
}
132220132315
else {
132221132316
(0, logger_1.logError)(`Not a valid comment body.`);
@@ -132266,8 +132361,11 @@ class AskActionUseCase {
132266132361
const embeddings = await this.dockerRepository.getEmbedding(param, [
132267132362
[this.CODE_INSTRUCTION_ASK, commentBody]
132268132363
]);
132269-
(0, logger_1.logInfo)(`🔎 Embeddings: ${JSON.stringify(embeddings, null, 2)}`);
132270-
const types = ['line', 'block'];
132364+
// logInfo(`🔎 Embeddings: ${JSON.stringify(embeddings, null, 2)}`);
132365+
const types = [
132366+
// 'line',
132367+
'block'
132368+
];
132271132369
const chunks = [];
132272132370
for (const type of types) {
132273132371
(0, logger_1.logInfo)(`📦 🔎 Matching chunks for ${param.owner}/${param.repo}/${param.commit.branch}`);
@@ -132277,6 +132375,74 @@ class AskActionUseCase {
132277132375
}
132278132376
chunks.push(...foundChunks);
132279132377
}
132378+
const { withContent, withoutContent } = await this.fileRepository.getFileTree(param.owner, param.repo, param.tokens.token, param.commit.branch, param.ai.getAiIgnoreFiles(), (fileName) => {
132379+
(0, logger_1.logSingleLine)(`Checking file ${fileName}`);
132380+
});
132381+
let workComplete = false;
132382+
let relatedFiles = new Map();
132383+
let finalResponse = '';
132384+
while (!workComplete) {
132385+
const prompt = `
132386+
You are a highly skilled code analysis assistant. I will provide you with:
132387+
1. A user's question about a codebase
132388+
2. A file tree representing the structure of the project
132389+
3. The most relevant code snippets from the codebase related to their query
132390+
132391+
Your tasks are:
132392+
- Analyze the code snippets in the context of the user's question.
132393+
- Use the file tree to provide additional context if needed (e.g., to understand module relationships).
132394+
- Provide your answer **only** in a JSON format, following this structure:
132395+
132396+
{
132397+
"text_response": "Your detailed analysis or answer here.",
132398+
"action": "none" | "analyze_files",
132399+
"related_files": ["optional", "list", "of", "files"],
132400+
"complete": true | false
132401+
}
132402+
132403+
Explanation:
132404+
- If the provided code snippets and file tree are sufficient to confidently answer the question, set "complete": true and "action": "none".
132405+
- If you determine that you need to review additional files to provide a complete and accurate answer, set "complete": false, "action": "analyze_files", and list the related file paths you need to investigate further in "related_files".
132406+
- Do not invent file paths; only request files that logically relate to the question based on the information available.
132407+
- Always provide a "text_response" with your reasoning, even if requesting more files.
132408+
132409+
Important:
132410+
- **Respond only with the JSON object**, without any extra commentary or text outside of the JSON.
132411+
132412+
Information provided:
132413+
User's question:
132414+
${commentBody}
132415+
132416+
File tree:
132417+
${JSON.stringify(withoutContent, null, 2)}
132418+
132419+
Relevant code snippets:
132420+
${relatedFiles.size > 0
132421+
? Array.from(relatedFiles.entries()).map(([path, content]) => `\nFile: ${path}\nCode:\n${content}`).join('\n')
132422+
: chunks.map(chunk => `\nFile: ${chunk.path}\nCode:\n${chunk.chunk}`).join('\n')}
132423+
`;
132424+
const jsonResponse = await this.aiRepository.askJson(param.ai, prompt);
132425+
if (!jsonResponse) {
132426+
(0, logger_1.logError)(`No result from AI.`);
132427+
results.push(new result_1.Result({
132428+
id: this.taskId,
132429+
success: false,
132430+
executed: true,
132431+
steps: [
132432+
`Error in ${this.taskId}: No result from AI.`,
132433+
],
132434+
}));
132435+
return results;
132436+
}
132437+
(0, logger_1.logInfo)(`🔎 Result: ${JSON.stringify(jsonResponse, null, 2)}`);
132438+
workComplete = jsonResponse.complete;
132439+
if (jsonResponse.action === 'analyze_files') {
132440+
relatedFiles = this.getRelatedFiles(jsonResponse.related_files, withContent);
132441+
}
132442+
else if (jsonResponse.action === 'none') {
132443+
finalResponse = jsonResponse.text_response;
132444+
}
132445+
}
132280132446
const totalDurationSeconds = (Date.now() - startTime) / 1000;
132281132447
(0, logger_1.logInfo)(`📦 🔎 Matched chunks for ${param.owner}/${param.repo}/${param.commit.branch}:\n Total duration: ${Math.ceil(totalDurationSeconds)} seconds`);
132282132448
results.push(new result_1.Result({
@@ -132304,6 +132470,30 @@ class AskActionUseCase {
132304132470
}
132305132471
return results;
132306132472
}
132473+
getRelatedFiles(relatedFiles, tree) {
132474+
const result = new Map();
132475+
const findFile = (node, targetPath) => {
132476+
if (node.path === targetPath) {
132477+
return node;
132478+
}
132479+
if (node.children) {
132480+
for (const child of node.children) {
132481+
const found = findFile(child, targetPath);
132482+
if (found) {
132483+
return found;
132484+
}
132485+
}
132486+
}
132487+
return null;
132488+
};
132489+
for (const filePath of relatedFiles) {
132490+
const fileNode = findFile(tree, filePath);
132491+
if (fileNode && fileNode.type === 'file' && fileNode.content) {
132492+
result.set(filePath, fileNode.content);
132493+
}
132494+
}
132495+
return result;
132496+
}
132307132497
}
132308132498
exports.AskActionUseCase = AskActionUseCase;
132309132499

dist/src/cli.js

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const child_process_1 = require("child_process");
4545
const boxen_1 = __importDefault(require("boxen"));
4646
const chalk_1 = __importDefault(require("chalk"));
4747
const logger_1 = require("./utils/logger");
48+
const issue_repository_1 = require("./data/repository/issue_repository");
4849
// Load environment variables from .env file
4950
dotenv.config();
5051
const program = new commander_1.Command();
@@ -119,20 +120,33 @@ program
119120
.option('-b, --branch <name>', 'Branch name', 'master')
120121
.option('-d, --debug', 'Debug mode', false)
121122
.option('-t, --token <token>', 'Personal access token', process.env.PERSONAL_ACCESS_TOKEN)
122-
.action((options) => {
123+
.option('-m, --model <model>', 'OpenRouter model', process.env.OPENROUTER_MODEL)
124+
.option('-k, --key <key>', 'OpenRouter API key', process.env.OPENROUTER_API_KEY)
125+
.option('-p, --provider <provider>', 'OpenRouter provider', process.env.OPENROUTER_PROVIDER_ORDER)
126+
.option('-f, --fallback <fallback>', 'OpenRouter fallback', process.env.OPENROUTER_PROVIDER_ALLOW_FALLBACKS)
127+
.option('-r, --require <require>', 'OpenRouter require', process.env.OPENROUTER_PROVIDER_REQUIRE_PARAMETERS)
128+
.option('-c, --collection <collection>', 'OpenRouter collection', process.env.OPENROUTER_PROVIDER_DATA_COLLECTION)
129+
.option('-q, --question <question>', 'Question', '')
130+
.action(async (options) => {
123131
const gitInfo = getGitInfo();
124132
if ('error' in gitInfo) {
125133
console.log(gitInfo.error);
126134
return;
127135
}
128-
const commentBody = `@landa-bot where is should add new constants?`;
136+
const commentBody = options.question;
129137
const params = {
130138
[constants_1.INPUT_KEYS.DEBUG]: options.debug.toString(),
131139
[constants_1.INPUT_KEYS.SINGLE_ACTION]: constants_1.ACTIONS.ASK,
132140
[constants_1.INPUT_KEYS.SINGLE_ACTION_ISSUE]: options.issue,
133141
[constants_1.INPUT_KEYS.SUPABASE_URL]: process.env.SUPABASE_URL,
134142
[constants_1.INPUT_KEYS.SUPABASE_KEY]: process.env.SUPABASE_KEY,
135143
[constants_1.INPUT_KEYS.TOKEN]: process.env.PERSONAL_ACCESS_TOKEN,
144+
[constants_1.INPUT_KEYS.OPENROUTER_API_KEY]: process.env.OPENROUTER_API_KEY,
145+
[constants_1.INPUT_KEYS.OPENROUTER_MODEL]: process.env.OPENROUTER_MODEL,
146+
[constants_1.INPUT_KEYS.OPENROUTER_PROVIDER_ORDER]: process.env.OPENROUTER_PROVIDER_ORDER,
147+
[constants_1.INPUT_KEYS.OPENROUTER_PROVIDER_ALLOW_FALLBACKS]: process.env.OPENROUTER_PROVIDER_ALLOW_FALLBACKS,
148+
[constants_1.INPUT_KEYS.OPENROUTER_PROVIDER_REQUIRE_PARAMETERS]: process.env.OPENROUTER_PROVIDER_REQUIRE_PARAMETERS,
149+
[constants_1.INPUT_KEYS.OPENROUTER_PROVIDER_DATA_COLLECTION]: process.env.OPENROUTER_PROVIDER_DATA_COLLECTION,
136150
[constants_1.INPUT_KEYS.AI_IGNORE_FILES]: 'dist/*,bin/*',
137151
repo: {
138152
owner: gitInfo.owner,
@@ -141,17 +155,28 @@ program
141155
commits: {
142156
ref: `refs/heads/${options.branch}`,
143157
},
144-
eventName: 'issue_comment',
145-
comment: {
146-
body: commentBody,
147-
},
148-
pull_request_review_comment: {
158+
};
159+
const issueRepository = new issue_repository_1.IssueRepository();
160+
const isIssue = await issueRepository.isIssue(gitInfo.owner, gitInfo.repo, parseInt(options.issue), process.env.PERSONAL_ACCESS_TOKEN ?? '');
161+
const isPullRequest = await issueRepository.isPullRequest(gitInfo.owner, gitInfo.repo, parseInt(options.issue), process.env.PERSONAL_ACCESS_TOKEN ?? '');
162+
if (isIssue) {
163+
params.eventName = 'issue_comment';
164+
params.issue = {
165+
number: parseInt(options.issue),
166+
};
167+
params.comment = {
149168
body: commentBody,
150-
},
151-
issue: {
169+
};
170+
}
171+
else if (isPullRequest) {
172+
params.eventName = 'pull_request_review_comment';
173+
params.issue = {
152174
number: parseInt(options.issue),
153-
},
154-
};
175+
};
176+
params.pull_request_review_comment = {
177+
body: commentBody,
178+
};
179+
}
155180
(0, logger_1.logInfo)((0, boxen_1.default)(chalk_1.default.cyan('🚀 Asking AI started\n') +
156181
chalk_1.default.gray(`Asking AI on ${gitInfo.owner}/${gitInfo.repo}/${options.branch}...`), {
157182
padding: 1,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface AiResponse {
2+
text_response: string;
3+
action: 'none' | 'analyze_files';
4+
related_files: string[];
5+
complete: boolean;
6+
}

dist/src/data/model/ai_response.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Ai } from '../model/ai';
22
export declare class AiRepository {
33
ask: (ai: Ai, prompt: string) => Promise<string | undefined>;
4+
askJson: (ai: Ai, prompt: string) => Promise<any | undefined>;
45
}

0 commit comments

Comments
 (0)