diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5bf8ce5..2c3a718 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -17,14 +17,14 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -39,7 +39,7 @@ jobs: # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + # Direct prompt for automated review (no @claude mention needed) direct_prompt: | Please review this pull request and provide feedback on: @@ -48,12 +48,12 @@ jobs: - Performance considerations - Security concerns - Test coverage - + Be constructive and helpful in your feedback. # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR # use_sticky_comment: true - + # Optional: Customize review based on file types # direct_prompt: | # Review this PR focusing on: @@ -61,18 +61,17 @@ jobs: # - For API endpoints: Security, input validation, and error handling # - For React components: Performance, accessibility, and best practices # - For tests: Coverage, edge cases, and test quality - + # Optional: Different prompts for different authors # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && + # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - + # Optional: Add specific tools for running tests or linting # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - + # Optional: Skip review for certain conditions # if: | # !contains(github.event.pull_request.title, '[skip-review]') && # !contains(github.event.pull_request.title, '[WIP]') - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 64a3e5b..fab4b58 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -39,26 +39,25 @@ jobs: # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read - + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" - + # Optional: Trigger when specific user is assigned to an issue # assignee_trigger: "claude-bot" - + # Optional: Allow Claude to run specific commands # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - + # Optional: Add custom instructions for Claude to customize its behavior for your project # custom_instructions: | # Follow our coding standards # Ensure all new code has tests # Use TypeScript for new files - + # Optional: Custom environment variables for Claude # claude_env: | # NODE_ENV: test - diff --git a/.vscode/launch.json b/.vscode/launch.json index 4bfc8ec..faa132f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,10 +24,7 @@ "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [ - "source/utils/__tests__/agent.test.ts", - "--runInBand" - ], + "args": ["source/utils/__tests__/agent.test.ts", "--runInBand"], "runtimeArgs": ["--loader", "ts-node/esm"], "console": "integratedTerminal", "skipFiles": ["/**"], diff --git a/source/services/agent-service.ts b/source/services/agent-service.ts index 97d61a9..73aaa5e 100644 --- a/source/services/agent-service.ts +++ b/source/services/agent-service.ts @@ -1,13 +1,11 @@ -import { agentLoop } from "../utils/agent.js"; -import { ThreadState } from "../utils/memory.js"; +import {agentLoop} from '../utils/agent.js'; +import {ThreadState} from '../utils/memory.js'; export class AgentService { - constructor( - private readonly state: ThreadState, - ) {} + constructor(private readonly state: ThreadState) {} async run(query: string) { const result = await agentLoop(query, this.state); return result; - } -} \ No newline at end of file + } +} diff --git a/source/utils/__tests__/agent.test.ts b/source/utils/__tests__/agent.test.ts index da59fa1..01ca329 100644 --- a/source/utils/__tests__/agent.test.ts +++ b/source/utils/__tests__/agent.test.ts @@ -1,11 +1,10 @@ +import {ThreadState} from '../memory.js'; +import {agentLoop} from '../agent.js'; +import ChatModels from '../../config/llm.js'; +import {toolsArray} from '../tools/index.js'; +import {Tool} from 'langchain/tools'; -import { ThreadState } from "../memory.js"; -import { agentLoop } from "../agent.js"; -import ChatModels from "../../config/llm.js"; -import { toolsArray } from "../tools/index.js"; -import { Tool } from "langchain/tools"; - -function handleEnv(test: boolean = false) { +function handleEnv(test: boolean = false) { if (test) { process.env['NODE_ENV'] = 'test'; process.env['OPENAI_API_KEY'] = 'test'; @@ -14,7 +13,6 @@ function handleEnv(test: boolean = false) { } } - describe.skip('Agent Utilities', () => { beforeAll(() => { handleEnv(true); @@ -35,10 +33,10 @@ describe.skip('Agent Utilities', () => { }; console.log(state); const result = await agentLoop( - 'What is current dir?', - state, - ChatModels.OPENAI_GPT_4_1_NANO, - toolsArray as Tool[] + 'What is current dir?', + state, + ChatModels.OPENAI_GPT_4_1_NANO, + toolsArray as Tool[], ); expect(result).toBeDefined(); // expect(result.content).toBe('Hello'); diff --git a/source/utils/agent.ts b/source/utils/agent.ts index 5239554..235fc2d 100644 --- a/source/utils/agent.ts +++ b/source/utils/agent.ts @@ -9,10 +9,10 @@ import { convertStateToXML, } from './memory.js'; import {classifyIntent} from './classify.js'; -import { toolsArray } from './tools/index.js'; +import {toolsArray} from './tools/index.js'; import {ToolIntent} from '../entities/tool.js'; import Prompt from '../config/prompt.js'; -import { Tool } from 'langchain/tools'; +import {Tool} from 'langchain/tools'; export interface AgentResponse { content: string; @@ -79,18 +79,21 @@ export async function executeTools( return state; } -export async function agentLoop( +export async function agentLoop( query: string, state: ThreadState, model: ChatModels = ChatModels.OPENAI_GPT_4_1_NANO, tools: Tool[] = toolsArray as Tool[], ): Promise { - // Add user input to memory state = await agentMemory('user_input', query, state); // Tool execution - classify all tools from the input at once - const [toolIntents, usage_metadata] = await classifyIntent(query, model.toString(), tools); + const [toolIntents, usage_metadata] = await classifyIntent( + query, + model.toString(), + tools, + ); // Execute all identified tools state = await executeTools(toolIntents, state); @@ -119,9 +122,15 @@ export async function agentLoop( const u1 = llmResponse.usage || {}; const u2 = usage_metadata || {}; return { - prompt_tokens: (u1.prompt_tokens || u1.input_tokens || 0) + (u2.input_tokens || u2.prompt_tokens || 0), - completion_tokens: (u1.completion_tokens || u1.output_tokens || 0) + (u2.output_tokens || u2.completion_tokens || 0), - total_tokens: (u1.total_tokens || u1.input_tokens + u1.output_tokens || 0) + (u2.total_tokens || u2.input_tokens + u2.output_tokens || 0), + prompt_tokens: + (u1.prompt_tokens || u1.input_tokens || 0) + + (u2.input_tokens || u2.prompt_tokens || 0), + completion_tokens: + (u1.completion_tokens || u1.output_tokens || 0) + + (u2.output_tokens || u2.completion_tokens || 0), + total_tokens: + (u1.total_tokens || u1.input_tokens + u1.output_tokens || 0) + + (u2.total_tokens || u2.input_tokens + u2.output_tokens || 0), }; })(); diff --git a/source/utils/classify.ts b/source/utils/classify.ts index 5ff5fcc..56baf76 100644 --- a/source/utils/classify.ts +++ b/source/utils/classify.ts @@ -1,29 +1,31 @@ import {getModel} from './llm.js'; import ChatModels from '../config/llm.js'; import {ToolIntent} from '../entities/tool.js'; -import { toolsArray } from './tools/index.js'; -import { Tool } from 'langchain/tools'; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { jsonToYaml } from './parse.js'; +import {toolsArray} from './tools/index.js'; +import {Tool} from 'langchain/tools'; +import {zodToJsonSchema} from 'zod-to-json-schema'; +import {jsonToYaml} from './parse.js'; export async function classifyIntent( query: string, modelName?: string, tools: Tool[] = toolsArray as Tool[], ): Promise<[ToolIntent[], any]> { - const prompt = `Analyze the following user query and identify ` + - `if any tools should be executed. Return a JSON array of tool intents. ` + - `If no tools are needed, return: [{"intent": "none", "args": {}}] + const prompt = + `Analyze the following user query and identify ` + + `if any tools should be executed. Return a JSON array of tool intents. ` + + `If no tools are needed, return: [{"intent": "none", "args": {}}] ## Available tools:\n~~~yaml -${tools.map((tool) => - - jsonToYaml({ - name: tool.name, - description: tool.description, - schema: zodToJsonSchema(tool.schema), - }) -).join('\n')}~~~ +${tools + .map(tool => + jsonToYaml({ + name: tool.name, + description: tool.description, + schema: zodToJsonSchema(tool.schema), + }), + ) + .join('\n')}~~~ User query: "${query}" @@ -58,7 +60,10 @@ Respond with only the JSON array, no additional text.`; cleanContent = jsonMatch[0]; } - return [JSON.parse(cleanContent), response.usage_metadata] as [ToolIntent[], any]; + return [JSON.parse(cleanContent), response.usage_metadata] as [ + ToolIntent[], + any, + ]; } catch (error) { console.error('Failed to parse LLM response:', content); console.error('Parse error:', error); diff --git a/source/utils/parse.ts b/source/utils/parse.ts index 8964d05..674c001 100644 --- a/source/utils/parse.ts +++ b/source/utils/parse.ts @@ -6,4 +6,4 @@ export const jsonToYaml = (json: any) => { export const yamlToJson = (yaml: string) => { return YAML.parse(yaml); -}; \ No newline at end of file +}; diff --git a/source/utils/tools/index.ts b/source/utils/tools/index.ts index 1d7a77c..91aa9cb 100644 --- a/source/utils/tools/index.ts +++ b/source/utils/tools/index.ts @@ -4,95 +4,70 @@ import {fileSearch} from './file-search.js'; import {readFile, createFile} from './file-operations.js'; import {gitStatus} from './git.js'; import {pwd, terminalCommand} from './system.js'; -import { tool } from '@langchain/core/tools'; -import { z } from 'zod'; +import {tool} from '@langchain/core/tools'; +import {z} from 'zod'; -export const mathCalculatorTool = tool( - mathCalculator, - { - name: 'math_calculator', - description: 'Calculate the result of a mathematical expression', - schema: z.object({ - expression: z.string().describe('The expression to calculate'), - }), - }, -); +export const mathCalculatorTool = tool(mathCalculator, { + name: 'math_calculator', + description: 'Calculate the result of a mathematical expression', + schema: z.object({ + expression: z.string().describe('The expression to calculate'), + }), +}); -export const readFileTool = tool( - readFile, - { - name: 'read_file', - description: 'Read the content of a file', - schema: z.object({ - filepath: z.string().describe('The path to the file'), - }), - }, -); +export const readFileTool = tool(readFile, { + name: 'read_file', + description: 'Read the content of a file', + schema: z.object({ + filepath: z.string().describe('The path to the file'), + }), +}); -export const createFileTool = tool( - createFile, - { - name: 'create_file', - description: 'Create a new file', - schema: z.object({ - filepath: z.string().describe('The path to the file'), - content: z.string().describe('The content of the file'), - }), - }, -); +export const createFileTool = tool(createFile, { + name: 'create_file', + description: 'Create a new file', + schema: z.object({ + filepath: z.string().describe('The path to the file'), + content: z.string().describe('The content of the file'), + }), +}); +export const fileSearchTool = tool(fileSearch, { + name: 'file_search', + description: 'Search for files in the current directory', + schema: z.object({ + pattern: z.string().describe('The pattern to search for'), + directory: z.string().describe('The directory to search in'), + }), +}); -export const fileSearchTool = tool( - fileSearch, - { - name: 'file_search', - description: 'Search for files in the current directory', - schema: z.object({ - pattern: z.string().describe('The pattern to search for'), - directory: z.string().describe('The directory to search in'), - }), - }, -); +export const webSearchTool = tool(webSearch, { + name: 'web_search', + description: 'Search the web for information', + schema: z.object({ + query: z.string().describe('The query to search for'), + }), +}); -export const webSearchTool = tool( - webSearch, - { - name: 'web_search', - description: 'Search the web for information', - schema: z.object({ - query: z.string().describe('The query to search for'), - }), - }, -); +export const pwdTool = tool(pwd, { + name: 'pwd', + description: 'Get the current working directory', + schema: z.object({}), +}); -export const pwdTool = tool( - pwd, - { - name: 'pwd', - description: 'Get the current working directory', - schema: z.object({}), - }, -); +export const terminalCommandTool = tool(terminalCommand, { + name: 'terminal_command', + description: 'Execute a terminal command', + schema: z.object({ + command: z.string().describe('The command to execute'), + timeout: z.number().describe('The timeout in milliseconds'), + }), +}); -export const terminalCommandTool = tool( - terminalCommand, - { - name: 'terminal_command', - description: 'Execute a terminal command', - schema: z.object({ - command: z.string().describe('The command to execute'), - timeout: z.number().describe('The timeout in milliseconds'), - }), - }, -); - -export const gitStatusTool = tool( - gitStatus, - { - name: 'git_status', - description: 'Get the status of the git repository', - }, -); +export const gitStatusTool = tool(gitStatus, { + name: 'git_status', + description: 'Get the status of the git repository', +}); export default { web_search: webSearch,