Skip to content
Merged
Show file tree
Hide file tree
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
19 changes: 9 additions & 10 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -48,31 +48,30 @@ 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:
# - For TypeScript files: Type safety and proper interface usage
# - 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]')

13 changes: 6 additions & 7 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

5 changes: 1 addition & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["<node_internals>/**"],
Expand Down
12 changes: 5 additions & 7 deletions source/services/agent-service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
22 changes: 10 additions & 12 deletions source/utils/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,7 +13,6 @@ function handleEnv(test: boolean = false) {
}
}


describe.skip('Agent Utilities', () => {
beforeAll(() => {
handleEnv(true);
Expand All @@ -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');
Expand Down
25 changes: 17 additions & 8 deletions source/utils/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AgentResponse> {

// 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);
Expand Down Expand Up @@ -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),
};
})();

Expand Down
37 changes: 21 additions & 16 deletions source/utils/classify.ts
Original file line number Diff line number Diff line change
@@ -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}"

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion source/utils/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export const jsonToYaml = (json: any) => {

export const yamlToJson = (yaml: string) => {
return YAML.parse(yaml);
};
};
Loading