-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add line-specific comments functionality #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3e180b0
39888e1
b55c00e
23b991b
81c2017
a8b94a7
c417dbe
f964b31
226617b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
{"promptStrategy":"modified-files"} | ||
{"promptStrategy":"line-comments"} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import Anthropic from '@anthropic-ai/sdk' | ||
|
||
/** | ||
* Default Anthropic sender. | ||
* This sender is used by default and sends the prompt to Anthropic | ||
* expecting a regular text response. | ||
* | ||
* @param prompt - The prompt to send to Anthropic | ||
* @returns The text response from Anthropic | ||
*/ | ||
export async function defaultSender(prompt: string): Promise<string> { | ||
// Initialize Anthropic client | ||
const anthropic = new Anthropic({ | ||
apiKey: process.env.ANTHROPIC_API_KEY | ||
}) | ||
|
||
// Send to Anthropic API | ||
const message = await anthropic.messages.create({ | ||
model: 'claude-3-7-sonnet-latest', | ||
max_tokens: 4096, | ||
temperature: 0, // Using 0 for consistent, deterministic code review feedback | ||
messages: [ | ||
{ | ||
role: 'user', | ||
content: prompt | ||
} | ||
] | ||
}) | ||
|
||
// Extract text from the content block | ||
if (message.content[0].type !== 'text') { | ||
throw new Error('Unexpected response type from Anthropic') | ||
} | ||
return message.content[0].text | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { defaultSender } from './default-sender.ts' | ||
import { lineCommentsSender } from './line-comments-sender.ts' | ||
|
||
/** | ||
* Type definition for all Anthropic senders | ||
*/ | ||
export type AnthropicSender = (prompt: string) => Promise<string> | ||
|
||
/** | ||
* Gets the appropriate sender based on the strategy name. | ||
* This selects how to send and process the response to/from Anthropic API. | ||
* | ||
* @param strategyName - The name of the prompt strategy used | ||
* @returns The appropriate sender function | ||
*/ | ||
export function getSender(strategyName?: string): AnthropicSender { | ||
switch (strategyName?.toLowerCase()) { | ||
case 'line-comments': | ||
return lineCommentsSender | ||
case 'default': | ||
case 'modified-files': | ||
default: | ||
return defaultSender | ||
} | ||
} | ||
|
||
export { defaultSender, lineCommentsSender } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import Anthropic from '@anthropic-ai/sdk' | ||
|
||
// Type for code review response | ||
interface CodeReviewResponse { | ||
summary: string | ||
comments: Array<{ | ||
path: string | ||
line: number | ||
body: string | ||
suggestion?: string | ||
}> | ||
} | ||
Comment on lines
+4
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Je pense que j'aurais plutôt mis ça dans le même fichier que le tool "provide_code_review" vu que c'est lié à ça plutôt qu'au |
||
|
||
/** | ||
* Line comments Anthropic sender. | ||
* This sender uses Anthropic's Tool Use / Function Calling capability | ||
* to enforce a structured JSON response with specific line-based comments. | ||
* | ||
* @param prompt - The prompt to send to Anthropic | ||
* @returns A stringified JSON response containing structured review comments | ||
*/ | ||
export async function lineCommentsSender(prompt: string): Promise<string> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dans l'idée c'est un "client" anthropic pour envoyer un prompt avec utilisation de tools. Dans ce cas, je l'appellerais simplement "anthropicClient", "anthropicSender", "anthropicClientWithTools" ou un truc du genre. J'irais même plus loin : Ça te paraît cohérent ? Dispo pour en parler si besoin |
||
// Initialize Anthropic client | ||
const anthropic = new Anthropic({ | ||
apiKey: process.env.ANTHROPIC_API_KEY | ||
}) | ||
|
||
// Send to Anthropic API with tool use configuration | ||
const message = await anthropic.messages.create({ | ||
model: 'claude-3-7-sonnet-latest', | ||
max_tokens: 4096, | ||
temperature: 0, | ||
messages: [ | ||
{ | ||
role: 'user', | ||
content: prompt | ||
} | ||
], | ||
tools: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Je me dis que ça pourrait être bien d'avoir un tool utils capable de construire un objet JSON "anthropicToolDefinition" (ou openAI, Mistral, LLaMa plus tard) automatiquement. Sinon on va devoir réécrire et maintenir cet objet dans chaque sender qui communique avec anthropic et dès qu'un tool va changer un peu ça risque de devenir embêtant à maintenir et source d'erreurs. On peut commencer simple avec une fonction qui renvoie simplement :
|
||
{ | ||
name: 'provide_code_review', | ||
description: | ||
'Provide structured code review with line-specific comments', | ||
input_schema: { | ||
type: 'object', | ||
properties: { | ||
summary: { | ||
type: 'string', | ||
description: 'Overall summary of the PR' | ||
}, | ||
comments: { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
properties: { | ||
path: { | ||
type: 'string', | ||
description: 'File path relative to repository root' | ||
}, | ||
line: { | ||
type: 'integer', | ||
description: 'Line number for the comment' | ||
}, | ||
body: { | ||
type: 'string', | ||
description: 'Detailed comment about the issue' | ||
}, | ||
suggestion: { | ||
type: 'string', | ||
description: 'Suggested code to fix the issue (optional)' | ||
} | ||
}, | ||
required: ['path', 'line', 'body'] | ||
} | ||
} | ||
}, | ||
required: ['summary', 'comments'] | ||
} | ||
} | ||
] | ||
}) | ||
|
||
// Extract response from tool use | ||
// Find content blocks that are tool_use type | ||
for (const content of message.content) { | ||
if (content.type === 'tool_use') { | ||
gary-van-woerkens marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (content.name === 'provide_code_review' && content.input) { | ||
gary-van-woerkens marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Return the structured response as a JSON string | ||
return JSON.stringify(content.input as CodeReviewResponse) | ||
} else { | ||
console.log('Input:', content.input) | ||
console.log('Tool name:', content.name) | ||
throw new Error('Tool name or input incorect') | ||
} | ||
} else { | ||
// Fallback if tool use failed or returned unexpected format | ||
if (content.type === 'text') { | ||
// Try to parse any JSON that might be in the response | ||
try { | ||
const text = content.text | ||
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/) | ||
if (jsonMatch && jsonMatch[1]) { | ||
return jsonMatch[1].trim() | ||
} | ||
// If the whole response is potentially JSON | ||
if (text.trim().startsWith('{') && text.trim().endsWith('}')) { | ||
return text | ||
} | ||
|
||
// Just return the text as is | ||
return text | ||
} catch { | ||
// Silent catch - continue to next content block or error | ||
} | ||
} | ||
} | ||
} | ||
|
||
throw new Error('Unexpected response format from Anthropic') | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { type Context, ProbotOctokit } from 'probot' | ||
|
||
type ListCommentsResponse = Awaited< | ||
ReturnType<ProbotOctokit['rest']['issues']['listComments']> | ||
> | ||
|
||
type SingleComment = ListCommentsResponse['data'][number] | ||
|
||
// Marker to identify our AI analysis comments | ||
const COMMENT_MARKER = '<!-- REVU-AI-ANALYSIS -->' | ||
|
||
/** | ||
* Find existing AI analysis comment by looking for the unique marker | ||
*/ | ||
async function findExistingAnalysisComment(context: Context, prNumber: number) { | ||
const repo = context.repo() | ||
|
||
// Get all comments on the PR | ||
const { data: comments } = await context.octokit.issues.listComments({ | ||
...repo, | ||
issue_number: prNumber | ||
}) | ||
|
||
// Find the comment with our marker | ||
return comments.find((comment) => comment.body.includes(COMMENT_MARKER)) | ||
} | ||
|
||
/** | ||
* Handles the creation or update of a global comment containing the analysis. | ||
* This is the original behavior of the application. | ||
*/ | ||
export async function globalCommentHandler( | ||
context: Context, | ||
prNumber: number, | ||
analysis: string | ||
) { | ||
// Format the analysis with our marker | ||
const formattedAnalysis = `${COMMENT_MARKER}\n\n${analysis}` | ||
|
||
// Check if we already have an analysis comment | ||
const existingComment = await findExistingAnalysisComment(context, prNumber) | ||
|
||
await upsertComment(context, existingComment, formattedAnalysis, prNumber) | ||
} | ||
|
||
export async function upsertComment( | ||
context: Context, | ||
existingComment: SingleComment, | ||
formattedAnalysis: string, | ||
prNumber: number | ||
) { | ||
const repo = context.repo() | ||
|
||
if (existingComment) { | ||
// Update the existing comment | ||
await context.octokit.issues.updateComment({ | ||
...repo, | ||
comment_id: existingComment.id, | ||
body: formattedAnalysis | ||
}) | ||
return `Updated existing analysis comment on PR #${prNumber}` | ||
} else { | ||
// Post a new comment | ||
await context.octokit.issues.createComment({ | ||
...repo, | ||
issue_number: prNumber, | ||
body: formattedAnalysis | ||
}) | ||
return `Created new analysis comment on PR #${prNumber}` | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { type Context } from 'probot' | ||
import { globalCommentHandler } from './global-comment-handler.ts' | ||
import { lineCommentsHandler } from './line-comments-handler.ts' | ||
|
||
/** | ||
* Callback type for comment handlers | ||
*/ | ||
export type CommentHandler = ( | ||
context: Context, | ||
prNumber: number, | ||
analysis: string | ||
) => Promise<string> | ||
|
||
/** | ||
* Gets the appropriate comment handler based on the strategy name. | ||
* This allows for different comment handling strategies based on the prompt strategy. | ||
* | ||
* @param strategyName - The name of the prompt strategy used | ||
* @returns The appropriate comment handler function | ||
*/ | ||
export function getCommentHandler(strategyName: string): CommentHandler { | ||
switch (strategyName.toLowerCase()) { | ||
case 'line-comments': | ||
return lineCommentsHandler | ||
case 'default': | ||
case 'modified-files': | ||
default: | ||
return globalCommentHandler | ||
} | ||
} | ||
|
||
export { globalCommentHandler, lineCommentsHandler } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
J'ai fait un commentaire plus bas qui permettrait d'éliminer ce
defaultSender
et de le remplacer par un "anthropicSender" avec les "tools" en paramètre optionnel.