Skip to content

Commit dbabe60

Browse files
authored
feat: pacts can be directly created as issues on github and also can be created via chatbot
2 parents ca6a8b9 + 6d6fe01 commit dbabe60

14 files changed

Lines changed: 1660 additions & 91 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v0.3.1
4+
### Added
5+
- Now Pacts can be created via ChatBot
6+
- Pacts can be used to directly create an issue on github
7+
38
## v0.3.0
49
### Added
510
- Pacts for tracking bugs, tasks, and features within projects

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</p>
1010

1111
<p align="center">
12-
<img src="https://img.shields.io/badge/version-0.3.0-blue" alt="Version 0.3.0" />
12+
<img src="https://img.shields.io/badge/version-0.3.1-blue" alt="Version 0.3.1" />
1313
<img src="https://img.shields.io/badge/status-experimental-orange" alt="Status: Experimental" />
1414
<a href="https://opensource.org/licenses/Apache-2.0">
1515
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
@@ -229,6 +229,9 @@ CLERK_WEBHOOK_SECRET=whsec_...
229229
# OpenAI API (Required)
230230
OPENAI_API_KEY=sk-...
231231

232+
# Parallel API (Required)
233+
PARALLEL_API_KEY=your_parallel_api_key
234+
232235
# Base URL (Required for local - use your ngrok URL)
233236
NEXT_PUBLIC_BASE_URL=https://your-ngrok-url.ngrok.io
234237

actions/project.ts

Lines changed: 316 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@ import { cache } from "react";
55
import { ChatOpenAI } from "@langchain/openai";
66
import { PromptTemplate } from "@langchain/core/prompts";
77
import { StringOutputParser } from "@langchain/core/output_parsers";
8-
import { ultraProjectChatBotPrompt } from "../prompts/ReverseArchitecture";
8+
import { ultraProjectChatBotPrompt, projectChatBotBugPrompt, projectChatBotTaskPrompt, projectChatBotFeaturePrompt } from "../prompts/ReverseArchitecture";
9+
import { createToolCallingAgent, AgentExecutor } from "langchain/agents";
10+
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
11+
import { searchCodeTool, getFileContentTool } from "./github/gitTools";
12+
import { getInstallationToken } from "./githubAppAuth";
13+
import Parallel from "parallel-web";
14+
import { DynamicStructuredTool } from "langchain/tools";
15+
import { z } from "zod";
916
const openaiKey = process.env.OPENAI_API_KEY;
17+
const parallelApiKey = process.env.PARALLEL_API_KEY;
18+
1019
const llm = new ChatOpenAI({
1120
openAIApiKey: openaiKey,
1221
model: "gpt-5-mini-2025-08-07"
@@ -19,6 +28,89 @@ const llm2 = new ChatOpenAI({
1928
const tool = {"type": "web_search_preview"}
2029
const llmWithWeb = llm2.bindTools([tool])
2130

31+
// Output schema for createPactProjectChatBot
32+
const pactOutputSchema = {
33+
type: "object",
34+
description: "Structured output containing a short response and pact details",
35+
properties: {
36+
shortResponse: {
37+
type: "string",
38+
description: "A concise response under 200 words explaining the analysis and recommendations"
39+
},
40+
pact: {
41+
type: "object",
42+
description: "The pact details with title and body",
43+
properties: {
44+
title: {
45+
type: "string",
46+
description: "Clear, actionable pact title"
47+
},
48+
body: {
49+
type: "string",
50+
description: "Detailed pact description in markdown format with context, approach, acceptance criteria, and code references"
51+
}
52+
},
53+
required: ["title", "body"]
54+
}
55+
},
56+
required: ["shortResponse", "pact"]
57+
};
58+
59+
// Parallel Web Search Tool
60+
const createParallelWebSearchTool = () => {
61+
const parallelClient = new Parallel({ apiKey: parallelApiKey });
62+
63+
return new DynamicStructuredTool({
64+
name: "parallelWebSearch",
65+
description: "Search the web for documentation, best practices, or external context related to React/Next.js development. Useful for finding latest information, framework documentation, or solutions to common problems.",
66+
schema: z.object({
67+
objective: z.string().describe("Clear objective describing what information you're looking for"),
68+
searchQueries: z.array(z.string()).describe("Array of specific search queries to execute"),
69+
maxResults: z.number().optional().default(10).describe("Maximum number of results to return per query (default: 10)")
70+
}),
71+
72+
func: async (input): Promise<string> => {
73+
const { objective, searchQueries, maxResults } = input as {
74+
objective: string,
75+
searchQueries: string[],
76+
maxResults?: number
77+
};
78+
79+
try {
80+
const searchResult = await parallelClient.beta.search({
81+
objective,
82+
search_queries: searchQueries,
83+
max_results: maxResults || 10,
84+
max_chars_per_result: 5000
85+
});
86+
87+
if (!searchResult.results || searchResult.results.length === 0) {
88+
return `No web search results found for objective: "${objective}"`;
89+
}
90+
91+
// Format results for better readability
92+
const formattedResults = searchResult.results.map((result, idx) => {
93+
const resultAny = result as any;
94+
return `Result ${idx + 1}:
95+
Title: ${result.title || 'N/A'}
96+
URL: ${result.url || 'N/A'}
97+
Content: ${resultAny.content ? resultAny.content.substring(0, 1000) : resultAny.text ? resultAny.text.substring(0, 1000) : 'No content'}
98+
---`;
99+
}).join('\n\n');
100+
101+
return `Web Search Results for "${objective}":
102+
103+
Total results: ${searchResult.results.length}
104+
105+
${formattedResults}`;
106+
107+
} catch (error) {
108+
return `Error performing web search: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
109+
}
110+
}
111+
});
112+
};
113+
22114
export interface ProjectMessage {
23115
id: string;
24116
type: 'user' | 'assistant';
@@ -95,11 +187,6 @@ export async function saveProjectArchitecture(
95187
beforeCommitHash?: string;
96188
}
97189
) {
98-
;
99-
100-
101-
;
102-
103190
try {
104191
// First verify the project belongs to the user
105192
const project = await db.project.findUnique({
@@ -450,8 +537,230 @@ export async function projectChatBot( userInput: string, projectFramework: strin
450537
return response;
451538
}
452539

453-
export async function createPactProjectChatBot() {
540+
export async function createPactProjectChatBot(
541+
projectId: string,
542+
userInput: string,
543+
pactType: 'BUG' | 'TASK' | 'FEATURE',
544+
conversationHistory: any[]
545+
): Promise<{ shortResponse: string; pact: { title: string; body: string } } | { error: string }> {
546+
try {
547+
// 1. Authentication & Authorization
548+
const { userId } = await auth();
549+
if (!userId) {
550+
return { error: 'Unauthorized' };
551+
}
552+
553+
// 2. Fetch Project Data
554+
const project = await db.project.findUnique({
555+
where: { id: projectId, userId: userId },
556+
select: {
557+
id: true,
558+
name: true,
559+
repoFullName: true,
560+
githubInstallationId: true,
561+
defaultBranch: true,
562+
framework: true,
563+
ProjectArchitecture: {
564+
orderBy: {
565+
createdAt: 'desc',
566+
},
567+
take: 1,
568+
},
569+
},
570+
});
571+
572+
if (!project) {
573+
return { error: 'Project not found' };
574+
}
575+
576+
if (!project.repoFullName) {
577+
return { error: 'Project repository not configured' };
578+
}
579+
580+
if (!project.githubInstallationId) {
581+
return { error: 'GitHub installation not configured for this project' };
582+
}
583+
584+
// 3. Get GitHub Access Token
585+
const installationTokenResult = await getInstallationToken(project.githubInstallationId);
586+
const accessToken = installationTokenResult.token;
587+
588+
// Extract owner and repo from repoFullName
589+
const [owner, repo] = project.repoFullName.split('/');
590+
if (!owner || !repo) {
591+
return { error: 'Invalid repository name format' };
592+
}
593+
594+
// Get latest architecture
595+
const latestArchitecture = project.ProjectArchitecture && project.ProjectArchitecture.length > 0
596+
? project.ProjectArchitecture[0]
597+
: null;
598+
599+
const projectArchitecture = latestArchitecture
600+
? JSON.stringify({
601+
components: latestArchitecture.components,
602+
rationale: latestArchitecture.architectureRationale
603+
})
604+
: 'No architecture available';
605+
606+
// 4. Initialize Tools
607+
const parallelWebSearchTool = createParallelWebSearchTool();
608+
609+
// We need to bind the GitHub tools with the specific parameters
610+
// Create bound versions of the tools
611+
const boundSearchCodeTool = new DynamicStructuredTool({
612+
name: "searchCode",
613+
description: searchCodeTool.description,
614+
schema: z.object({
615+
query: z.string().describe("Search query (e.g., 'useEffect', '@nestjs/', 'function component')"),
616+
language: z.string().optional().describe("Filter by programming language (e.g., 'typescript', 'javascript')"),
617+
extension: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'tsx', 'js')"),
618+
path: z.string().optional().describe("Filter by file path pattern (e.g., 'src/', 'components/')"),
619+
}),
620+
func: async (input: any) => {
621+
return await searchCodeTool.func({
622+
...input,
623+
owner,
624+
repo,
625+
accessToken
626+
});
627+
}
628+
});
629+
630+
const boundGetFileContentTool = new DynamicStructuredTool({
631+
name: "getFileContent",
632+
description: getFileContentTool.description,
633+
schema: z.object({
634+
path: z.string().describe("File path within the repository (e.g., 'src/app/page.tsx')"),
635+
branch: z.string().optional().describe("Branch name (optional, uses default branch if not specified)"),
636+
}),
637+
func: async (input: any) => {
638+
return await getFileContentTool.func({
639+
...input,
640+
owner,
641+
repo,
642+
accessToken,
643+
branch: input.branch || project.defaultBranch || 'main'
644+
});
645+
}
646+
});
647+
648+
const tools = [boundSearchCodeTool, boundGetFileContentTool, parallelWebSearchTool];
649+
650+
// 5. Format Conversation History
651+
const formattedHistory = conversationHistory.map(msg =>
652+
`${msg.type === 'user' ? 'User' : 'Assistant'}: ${msg.content}`
653+
).join('\n');
654+
655+
const pactPrompt = pactType === 'BUG' ? projectChatBotBugPrompt : pactType === 'TASK' ? projectChatBotTaskPrompt : projectChatBotFeaturePrompt;
656+
657+
// 6. Create Agent
658+
const prompt = ChatPromptTemplate.fromMessages([
659+
["system", pactPrompt],
660+
new MessagesPlaceholder("agent_scratchpad"),
661+
]);
662+
663+
const agent = await createToolCallingAgent({
664+
llm,
665+
tools,
666+
prompt,
667+
});
668+
669+
const agentExecutor = new AgentExecutor({
670+
agent,
671+
tools,
672+
verbose: true,
673+
maxIterations: 15, // Reduced to encourage fewer tool calls and lower costs
674+
});
675+
676+
// 7. Execute Agent
677+
const agentResult = await agentExecutor.invoke({
678+
userInput,
679+
projectArchitecture,
680+
framework: project.framework || 'Unknown',
681+
conversationHistory: formattedHistory,
682+
repoFullName: project.repoFullName,
683+
});
684+
685+
// Check if agent completed successfully
686+
if (!agentResult.output || typeof agentResult.output !== 'string') {
687+
return { error: 'Agent failed to generate a response. Please try again with a simpler request.' };
688+
}
689+
690+
// Check for max iterations error
691+
if (agentResult.output.includes('Agent stopped due to max iterations') ||
692+
agentResult.output.includes('Agent stopped due to iteration limit')) {
693+
return { error: 'The request was too complex and exceeded the processing limit. Please try breaking it into smaller tasks.' };
694+
}
695+
696+
// 8. Structure Output using LLM with structured output
697+
// const structuredLlm = llm.withStructuredOutput(pactOutputSchema);
698+
699+
// Create a prompt to format the agent output into our schema
700+
// const formatterPrompt = PromptTemplate.fromTemplate(`
701+
// You are formatting an agent's analysis into a structured pact format.
702+
703+
// The agent analyzed this request and provided the following output. Your job is to extract and format it properly.
454704

705+
// Agent's Analysis:
706+
// {agentOutput}
707+
708+
// Pact Type: {pactType}
709+
710+
// Create a structured response with:
711+
// 1. shortResponse: Extract or create a concise summary (under 200 words) of the key findings and recommendations
712+
// 2. pact.title: Create a clear, actionable title for this {pactType}
713+
// 3. pact.body: Format the detailed analysis as markdown following the {pactType} template
714+
715+
// IMPORTANT: The pact body should be well-formatted markdown with proper sections based on the pact type.
716+
// `);
717+
718+
// const formatterChain = formatterPrompt.pipe(structuredLlm);
719+
720+
// const structuredOutput = await formatterChain.invoke({
721+
// agentOutput: agentCopyResult,
722+
// pactType
723+
// });
724+
725+
// // Validate structured output
726+
// if (!structuredOutput || !structuredOutput.shortResponse || !structuredOutput.pact) {
727+
// return { error: 'Failed to generate properly formatted pact. Please try again.' };
728+
// }
729+
730+
// if (!structuredOutput.pact.title || !structuredOutput.pact.body) {
731+
// return { error: 'Generated pact is missing required fields. Please try again.' };
732+
// }
733+
734+
const structuredOutput = JSON.parse(agentResult.output);
735+
736+
return {
737+
shortResponse: structuredOutput.shortResponse,
738+
pact: {
739+
title: structuredOutput.pact.title,
740+
body: structuredOutput.pact.body
741+
}
742+
};
743+
744+
} catch (error) {
745+
console.error('Error in createPactProjectChatBot:', error);
746+
747+
// Handle specific error types
748+
if (error instanceof Error) {
749+
if (error.message.includes('rate limit')) {
750+
return { error: 'GitHub API rate limit exceeded. Please try again later.' };
751+
}
752+
if (error.message.includes('authentication')) {
753+
return { error: 'GitHub authentication failed. Please reconnect your repository.' };
754+
}
755+
if (error.message.includes('timeout')) {
756+
return { error: 'Request timeout. The operation took too long. Please try again.' };
757+
}
758+
759+
return { error: `Failed to generate pact: ${error.message}` };
760+
}
761+
762+
return { error: 'An unexpected error occurred while generating the pact' };
763+
}
455764
}
456765

457766

0 commit comments

Comments
 (0)