From 2dd495be2fe4ad8d8046e600f2c143fe8256be2d Mon Sep 17 00:00:00 2001 From: AJAmit17 Date: Tue, 3 Mar 2026 19:10:47 +0530 Subject: [PATCH 1/6] feat: add exportToLangChain function and provider detection in langchain adapter --- src/adapters/index.ts | 1 + src/adapters/langchain.ts | 450 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 src/adapters/langchain.ts diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 2bedd3e..6b095d0 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -4,3 +4,4 @@ export { exportToOpenAI } from './openai.js'; export { exportToCrewAI } from './crewai.js'; export { exportToOpenClawString, exportToOpenClaw } from './openclaw.js'; export { exportToNanobotString, exportToNanobot } from './nanobot.js'; +export { exportToLangChain } from './langchain.js'; diff --git a/src/adapters/langchain.ts b/src/adapters/langchain.ts new file mode 100644 index 0000000..a0c90cc --- /dev/null +++ b/src/adapters/langchain.ts @@ -0,0 +1,450 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import yaml from 'js-yaml'; +import { loadAgentManifest, loadFileIfExists, AgentManifest } from '../utils/loader.js'; +import { loadAllSkills, getAllowedTools } from '../utils/skill-loader.js'; + +// --------------------------------------------------------------------------- +// Provider detection — maps model name prefixes to pip packages + env vars +// --------------------------------------------------------------------------- +interface ProviderInfo { + provider: string; + pipPackage: string; + envVar: string; +} + +function detectProvider(model: string): ProviderInfo { + const m = model.toLowerCase(); + if (m.startsWith('claude')) return { provider: 'anthropic', pipPackage: 'langchain-anthropic', envVar: 'ANTHROPIC_API_KEY' }; + if (m.startsWith('gemini')) return { provider: 'google_genai', pipPackage: 'langchain-google-genai', envVar: 'GOOGLE_API_KEY' }; + if (m.startsWith('grok')) return { provider: 'xai', pipPackage: 'langchain-xai', envVar: 'XAI_API_KEY' }; + if (m.startsWith('mistral')) return { provider: 'mistralai', pipPackage: 'langchain-mistralai', envVar: 'MISTRAL_API_KEY' }; + if (m.startsWith('deepseek')) return { provider: 'deepseek', pipPackage: 'langchain-deepseek', envVar: 'DEEPSEEK_API_KEY' }; + if (m.startsWith('command')) return { provider: 'cohere', pipPackage: 'langchain-cohere', envVar: 'COHERE_API_KEY' }; + // Default: OpenAI (covers gpt-*, o1-*, o3-*, etc.) + return { provider: 'openai', pipPackage: 'langchain-openai', envVar: 'OPENAI_API_KEY' }; +} + +// Make detectProvider available for tests +export { detectProvider }; + +export function exportToLangChain(dir: string): string { + const agentDir = resolve(dir); + const manifest = loadAgentManifest(agentDir); + + // Build system prompt (with knowledge + memory inlined) + const systemPrompt = buildSystemPrompt(agentDir, manifest); + + // Build tools array + const tools = buildToolDefinitions(agentDir, manifest); + + // Detect sub-agents + const subAgents = buildSubAgentDefinitions(agentDir, manifest); + + // Detect memory config + const hasMemory = existsSync(join(agentDir, 'memory', 'memory.yaml')) || + existsSync(join(agentDir, 'memory', 'MEMORY.md')); + + // Determine LLM provider and model from manifest + const model = manifest.model?.preferred ?? 'gpt-4o'; + const temperature = manifest.model?.constraints?.temperature ?? 0.7; + const maxTokens = manifest.model?.constraints?.max_tokens; + + // Collect all providers needed (main + sub-agents) + const mainProvider = detectProvider(model); + const allPipPackages = new Set([mainProvider.pipPackage]); + const allEnvVars = new Set([mainProvider.envVar]); + + for (const sub of subAgents) { + const subProv = detectProvider(sub.model ?? model); + allPipPackages.add(subProv.pipPackage); + allEnvVars.add(subProv.envVar); + } + + const pipPackages = ['langchain', ...allPipPackages]; + + // Generate Python code for LangChain + const lines: string[] = []; + + // --- Docstring --- + lines.push('"""'); + lines.push(`LangChain agent definition for ${manifest.name} v${manifest.version}`); + lines.push('Generated by gitagent export'); + lines.push(''); + lines.push('Prerequisites:'); + lines.push(` pip install ${pipPackages.join(' ')}`); + lines.push(''); + lines.push('What is mapped:'); + lines.push(' - System prompt (SOUL.md, RULES.md, skills, compliance)'); + lines.push(' - Tool stubs from tools/*.yaml'); + lines.push(' - Model and temperature from agent.yaml'); + lines.push(' - Knowledge documents (always_load) inlined into system prompt'); + if (manifest.runtime?.max_turns) { + lines.push(' - max_turns → recursion_limit'); + } + if (hasMemory) { + lines.push(' - Memory managed automatically by create_agent'); + } + if (subAgents.length > 0) { + lines.push(` - Sub-agents as tool delegates: ${subAgents.map(s => s.name).join(', ')}`); + } + if (manifest.delegation) { + lines.push(' - Delegation instructions embedded in system prompt'); + } + lines.push('"""\n'); + + // --- Imports (universal — no provider-specific imports) --- + lines.push('import os'); + lines.push('import sys'); + lines.push('from langchain.chat_models import init_chat_model'); + lines.push('from langchain.agents import create_agent'); + lines.push('from langchain.tools import tool'); + lines.push(''); + + // --- Tool stub definitions (module level — no API key needed) --- + if (tools.length > 0) { + lines.push('# --- Tool definitions ---\n'); + for (const t of tools) { + const funcName = t.name.replace(/-/g, '_'); + lines.push('@tool'); + lines.push(`def ${funcName}(${t.params}) -> str:`); + lines.push(` """${t.description}"""`); + lines.push(' # TODO: Implement tool logic'); + lines.push(' return "Not implemented"'); + lines.push(''); + } + } + + // --- System prompt (module level) --- + const escapedPrompt = systemPrompt + .replace(/\\/g, '\\\\') + .replace(/"""/g, '\\"\\"\\"'); + + lines.push(`SYSTEM_PROMPT = """${escapedPrompt}"""`); + lines.push(''); + + // --- Main block --- + // All model/agent creation happens here, AFTER env var checks, + // so providers auto-detect API keys from environment variables. + const envVarChecks = [...allEnvVars]; + const agentVarName = manifest.name.replace(/-/g, '_'); + + lines.push('if __name__ == "__main__":'); + for (const envVar of envVarChecks) { + lines.push(` if not os.environ.get("${envVar}"):`); + lines.push(` print("Error: ${envVar} environment variable is not set")`); + lines.push(' sys.exit(1)'); + lines.push(''); + } + + // Sub-agent delegates inside main (they require API keys) + if (subAgents.length > 0) { + lines.push(' # --- Sub-agent delegates ---'); + lines.push(' # Each sub-agent is a full agent the main agent can invoke via a tool.\n'); + + for (const sub of subAgents) { + const subVarName = sub.name.replace(/-/g, '_'); + const subModel = sub.model ?? model; + const subTemp = sub.temperature ?? 0.7; + + const subPromptEscaped = sub.systemPrompt + .replace(/\\/g, '\\\\') + .replace(/"""/g, '\\"\\"\\"'); + + const subModelArgs: string[] = [`"${subModel}"`, `temperature=${subTemp}`]; + + lines.push(` _${subVarName} = create_agent(`); + lines.push(` model=init_chat_model(${subModelArgs.join(', ')}),`); + lines.push(' tools=[],'); + lines.push(` system_prompt="""${subPromptEscaped}""",`); + lines.push(` name="${subVarName}",`); + lines.push(' )'); + lines.push(''); + + lines.push(' @tool'); + lines.push(` def delegate_to_${subVarName}(query: str) -> str:`); + lines.push(` """${sub.description} — Delegate a task to the ${sub.name} sub-agent."""`); + lines.push(` result = _${subVarName}.invoke(`); + lines.push(' {"messages": [{"role": "user", "content": query}]}'); + lines.push(' )'); + lines.push(' return result["messages"][-1].content'); + lines.push(''); + } + } + + // Tools list (including sub-agent delegates + tool stubs) + const allToolNames: string[] = []; + for (const sub of subAgents) { + allToolNames.push(`delegate_to_${sub.name.replace(/-/g, '_')}`); + } + for (const t of tools) { + allToolNames.push(t.name.replace(/-/g, '_')); + } + + if (allToolNames.length > 0) { + lines.push(` tools = [${allToolNames.join(', ')}]`); + } else { + lines.push(' tools = []'); + } + lines.push(''); + + // Model init args (no api_key — providers auto-detect from env vars) + const modelInitArgs: string[] = [`"${model}"`, `temperature=${temperature}`]; + if (maxTokens) { + modelInitArgs.push(`max_tokens=${maxTokens}`); + } + + // Agent creation + lines.push(` ${agentVarName} = create_agent(`); + lines.push(` model=init_chat_model(${modelInitArgs.join(', ')}),`); + lines.push(' tools=tools,'); + lines.push(' system_prompt=SYSTEM_PROMPT,'); + lines.push(` name="${agentVarName}",`); + lines.push(' )'); + lines.push(''); + + // Invocation + lines.push(' user_input = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Hello, what can you help me with?"'); + + if (manifest.runtime?.max_turns) { + lines.push(` result = ${agentVarName}.invoke(`); + lines.push(' {"messages": [{"role": "user", "content": user_input}]},'); + lines.push(` {"recursion_limit": ${manifest.runtime.max_turns * 2}},`); + lines.push(' )'); + } else { + lines.push(` result = ${agentVarName}.invoke(`); + lines.push(' {"messages": [{"role": "user", "content": user_input}]}'); + lines.push(' )'); + } + + lines.push(' print(result["messages"][-1].content)'); + lines.push(''); + + return lines.join('\n'); +} + +function buildSystemPrompt(agentDir: string, manifest: AgentManifest): string { + const parts: string[] = []; + + // Agent identity header + parts.push(`# ${manifest.name} v${manifest.version}`); + parts.push(`${manifest.description}\n`); + + const soul = loadFileIfExists(join(agentDir, 'SOUL.md')); + if (soul) parts.push(soul); + + const rules = loadFileIfExists(join(agentDir, 'RULES.md')); + if (rules) parts.push(rules); + + const duties = loadFileIfExists(join(agentDir, 'DUTIES.md')); + if (duties) parts.push(duties); + + // Skills — loaded via skill-loader + const skillsDir = join(agentDir, 'skills'); + const skills = loadAllSkills(skillsDir); + for (const skill of skills) { + const toolsList = getAllowedTools(skill.frontmatter); + const toolsNote = toolsList.length > 0 ? `\nAllowed tools: ${toolsList.join(', ')}` : ''; + parts.push(`## Skill: ${skill.frontmatter.name}\n${skill.frontmatter.description}${toolsNote}\n\n${skill.instructions}`); + } + + // Knowledge (always_load documents) — inlined into prompt + const knowledgeDir = join(agentDir, 'knowledge'); + const indexPath = join(knowledgeDir, 'index.yaml'); + if (existsSync(indexPath)) { + const index = yaml.load(readFileSync(indexPath, 'utf-8')) as { + documents?: Array<{ path: string; always_load?: boolean }>; + }; + + if (index.documents) { + const alwaysLoad = index.documents.filter(d => d.always_load); + for (const doc of alwaysLoad) { + const content = loadFileIfExists(join(knowledgeDir, doc.path)); + if (content) { + parts.push(`## Knowledge: ${doc.path}\n${content}`); + } + } + } + } + + // Compliance constraints + if (manifest.compliance) { + const c = manifest.compliance; + const constraints: string[] = ['## Compliance Constraints']; + + if (c.supervision?.human_in_the_loop === 'always') { + constraints.push('- All decisions require human approval before execution'); + } + if (c.supervision?.escalation_triggers) { + constraints.push('- Escalate to human supervisor when:'); + for (const trigger of c.supervision.escalation_triggers) { + for (const [key, value] of Object.entries(trigger)) { + constraints.push(` - ${key}: ${value}`); + } + } + } + if (c.communications?.fair_balanced) constraints.push('- All communications must be fair and balanced (FINRA 2210)'); + if (c.communications?.no_misleading) constraints.push('- Never make misleading, exaggerated, or promissory statements'); + if (c.data_governance?.pii_handling === 'redact') constraints.push('- Redact all PII from outputs and intermediate reasoning'); + if (c.data_governance?.pii_handling === 'prohibit') constraints.push('- Do not process any personally identifiable information'); + + if (c.segregation_of_duties) { + const sod = c.segregation_of_duties; + if (sod.assignments) { + constraints.push('- Segregation of duties is enforced:'); + for (const [agentName, roles] of Object.entries(sod.assignments)) { + constraints.push(` - Agent "${agentName}" has role(s): ${roles.join(', ')}`); + } + } + if (sod.conflicts) { + constraints.push('- Duty separation rules (no single agent may hold both):'); + for (const [a, b] of sod.conflicts) { + constraints.push(` - ${a} and ${b}`); + } + } + if (sod.enforcement === 'strict') { + constraints.push('- SOD enforcement is STRICT — violations will block execution'); + } + } + + if (constraints.length > 1) parts.push(constraints.join('\n')); + } + + // Delegation instructions + if (manifest.delegation) { + const delParts: string[] = ['## Delegation']; + delParts.push(`Mode: ${manifest.delegation.mode ?? 'manual'}`); + if (manifest.agents) { + delParts.push('Available sub-agents you can delegate to:'); + for (const [name, config] of Object.entries(manifest.agents)) { + const funcName = `delegate_to_${name.replace(/-/g, '_')}`; + delParts.push(`- Use the "${funcName}" tool to delegate to ${name}: ${config.description ?? ''}`); + if (config.delegation?.triggers) { + delParts.push(` Triggers: ${config.delegation.triggers.join(', ')}`); + } + } + } + parts.push(delParts.join('\n')); + } + + // Memory + const memory = loadFileIfExists(join(agentDir, 'memory', 'MEMORY.md')); + if (memory && memory.trim().split('\n').length > 2) { + parts.push(`## Memory\n${memory}`); + } + + return parts.join('\n\n'); +} + +interface SubAgentDef { + name: string; + description: string; + model: string | undefined; + temperature: number | undefined; + systemPrompt: string; +} + +function buildSubAgentDefinitions(agentDir: string, manifest: AgentManifest): SubAgentDef[] { + const subAgents: SubAgentDef[] = []; + + if (!manifest.agents) return subAgents; + + for (const [name, config] of Object.entries(manifest.agents)) { + const subDir = join(agentDir, 'agents', name); + + // Try to load the sub-agent's own manifest + SOUL + let subModel: string | undefined; + let subTemp: number | undefined; + let subPromptParts: string[] = []; + + if (existsSync(join(subDir, 'agent.yaml'))) { + try { + const subManifest = loadAgentManifest(subDir); + subModel = subManifest.model?.preferred; + subTemp = subManifest.model?.constraints?.temperature; + subPromptParts.push(`You are ${subManifest.name}: ${subManifest.description}`); + } catch { /* ignore */ } + } + + const subSoul = loadFileIfExists(join(subDir, 'SOUL.md')); + if (subSoul) subPromptParts.push(subSoul); + + const subDuties = loadFileIfExists(join(subDir, 'DUTIES.md')); + if (subDuties) subPromptParts.push(subDuties); + + // Fallback description + if (subPromptParts.length === 0) { + subPromptParts.push(config.description ?? `Sub-agent: ${name}`); + } + + subAgents.push({ + name, + description: config.description ?? `Delegate tasks to ${name}`, + model: subModel, + temperature: subTemp, + systemPrompt: subPromptParts.join('\n\n'), + }); + } + + return subAgents; +} + +interface ToolDef { + name: string; + description: string; + params: string; +} + +function buildToolDefinitions(agentDir: string, _manifest: AgentManifest): ToolDef[] { + const tools: ToolDef[] = []; + const toolsDir = join(agentDir, 'tools'); + + if (!existsSync(toolsDir)) return tools; + + const files = readdirSync(toolsDir).filter(f => f.endsWith('.yaml')); + + for (const file of files) { + const content = readFileSync(join(toolsDir, file), 'utf-8'); + const toolConfig = yaml.load(content) as { + name: string; + description: string; + input_schema?: { + properties?: Record; + required?: string[]; + }; + }; + + const params: string[] = []; + if (toolConfig.input_schema?.properties) { + for (const [name, schema] of Object.entries(toolConfig.input_schema.properties)) { + const pyType = jsonTypeToPython(schema.type); + const isRequired = toolConfig.input_schema.required?.includes(name); + if (isRequired) { + params.push(`${name}: ${pyType}`); + } else { + params.push(`${name}: ${pyType} = None`); + } + } + } + + tools.push({ + name: toolConfig.name, + description: toolConfig.description, + params: params.join(', '), + }); + } + + return tools; +} + +function jsonTypeToPython(jsonType: string): string { + switch (jsonType) { + case 'string': return 'str'; + case 'integer': return 'int'; + case 'number': return 'float'; + case 'boolean': return 'bool'; + case 'array': return 'list'; + case 'object': return 'dict'; + default: return 'str'; + } +} From 5c5e543ca54c772ecfcbc5f48cdf9b03e0b0cce8 Mon Sep 17 00:00:00 2001 From: AJAmit17 Date: Tue, 3 Mar 2026 19:10:56 +0530 Subject: [PATCH 2/6] feat: add runWithLangChain function to execute LangChain agents --- src/runners/git.ts | 4 ++++ src/runners/langchain.ts | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/runners/langchain.ts diff --git a/src/runners/git.ts b/src/runners/git.ts index 7e11a2d..11abfa1 100644 --- a/src/runners/git.ts +++ b/src/runners/git.ts @@ -11,6 +11,7 @@ import { runWithOpenClaw } from './openclaw.js'; import { runWithNanobot } from './nanobot.js'; import { runWithLyzr } from './lyzr.js'; import { runWithGitHub } from './github.js'; +import { runWithLangChain } from './langchain.js'; import { error, info, success, label, heading, divider, warn } from '../utils/format.js'; export interface GitRunOptions { @@ -109,6 +110,9 @@ export async function runWithGit( case 'github': await runWithGitHub(agentDir, manifest, { prompt: options.prompt }); break; + case 'langchain': + runWithLangChain(agentDir, manifest); + break; case 'prompt': console.log(exportToSystemPrompt(agentDir)); break; diff --git a/src/runners/langchain.ts b/src/runners/langchain.ts new file mode 100644 index 0000000..fb89e05 --- /dev/null +++ b/src/runners/langchain.ts @@ -0,0 +1,49 @@ +import { writeFileSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawnSync } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; +import { exportToLangChain, detectProvider } from '../adapters/langchain.js'; +import { AgentManifest } from '../utils/loader.js'; +import { error, info } from '../utils/format.js'; + +export function runWithLangChain(agentDir: string, _manifest: AgentManifest): void { + const model = _manifest.model?.preferred ?? 'gpt-4o'; + const providerInfo = detectProvider(model); + + // Check the appropriate API key env var + const apiKey = process.env[providerInfo.envVar]; + if (!apiKey) { + error(`${providerInfo.envVar} environment variable is not set`); + info(`Set it with: export ${providerInfo.envVar}="your-key-here"`); + process.exit(1); + } + + const script = exportToLangChain(agentDir); + const tmpFile = join(tmpdir(), `gitagent-langchain-${randomBytes(4).toString('hex')}.py`); + + writeFileSync(tmpFile, script, 'utf-8'); + + const pipHint = `langchain ${providerInfo.pipPackage}`; + info(`Running LangChain agent from "${agentDir}"...`); + info(`Requires: pip install ${pipHint}`); + + try { + const result = spawnSync('python3', [tmpFile], { + stdio: 'inherit', + cwd: agentDir, + env: { ...process.env }, + }); + + if (result.error) { + error(`Failed to run Python: ${result.error.message}`); + info('Make sure python3 is installed and langchain packages are available:'); + info(` pip install ${pipHint}`); + process.exit(1); + } + + process.exit(result.status ?? 0); + } finally { + try { unlinkSync(tmpFile); } catch { /* ignore */ } + } +} From a176f98ef54e53019b0bb34a6d595b44d8643fe2 Mon Sep 17 00:00:00 2001 From: AJAmit17 Date: Tue, 3 Mar 2026 19:11:05 +0530 Subject: [PATCH 3/6] feat: add langchain support to export and run commands --- src/commands/export.ts | 8 ++++++-- src/commands/run.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/commands/export.ts b/src/commands/export.ts index 83bb2af..1f0a1a6 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -8,6 +8,7 @@ import { exportToCrewAI, exportToOpenClawString, exportToNanobotString, + exportToLangChain, } from '../adapters/index.js'; import { exportToLyzrString } from '../adapters/lyzr.js'; import { exportToGitHubString } from '../adapters/github.js'; @@ -20,7 +21,7 @@ interface ExportOptions { export const exportCommand = new Command('export') .description('Export agent to other formats') - .requiredOption('-f, --format ', 'Export format (system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github)') + .requiredOption('-f, --format ', 'Export format (system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, langchain)') .option('-d, --dir ', 'Agent directory', '.') .option('-o, --output ', 'Output file path') .action(async (options: ExportOptions) => { @@ -57,9 +58,12 @@ export const exportCommand = new Command('export') case 'github': result = exportToGitHubString(dir); break; + case 'langchain': + result = exportToLangChain(dir); + break; default: error(`Unknown format: ${options.format}`); - info('Supported formats: system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github'); + info('Supported formats: system-prompt, claude-code, openai, crewai, openclaw, nanobot, lyzr, github, langchain'); process.exit(1); } diff --git a/src/commands/run.ts b/src/commands/run.ts index 4773dd5..77234d6 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -12,6 +12,7 @@ import { runWithNanobot } from '../runners/nanobot.js'; import { runWithLyzr } from '../runners/lyzr.js'; import { runWithGitHub } from '../runners/github.js'; import { runWithGit } from '../runners/git.js'; +import { runWithLangChain } from '../runners/langchain.js'; interface RunOptions { repo?: string; @@ -26,7 +27,7 @@ interface RunOptions { export const runCommand = new Command('run') .description('Run an agent from a git repository or local directory') .option('-r, --repo ', 'Git repository URL') - .option('-a, --adapter ', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, git, prompt', 'claude') + .option('-a, --adapter ', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, langchain, git, prompt', 'claude') .option('-b, --branch ', 'Git branch/tag to clone', 'main') .option('--refresh', 'Force re-clone (pull latest)', false) .option('--no-cache', 'Clone to temp dir, delete on exit') @@ -112,6 +113,9 @@ export const runCommand = new Command('run') case 'github': await runWithGitHub(agentDir, manifest, { prompt: options.prompt }); break; + case 'langchain': + runWithLangChain(agentDir, manifest); + break; case 'git': if (!options.repo) { error('The git adapter requires --repo (-r)'); @@ -130,7 +134,7 @@ export const runCommand = new Command('run') break; default: error(`Unknown adapter: ${options.adapter}`); - info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, git, prompt'); + info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, langchain, git, prompt'); process.exit(1); } } catch (e) { From 04aa89b2e470e0a8bda2b6aa49d746569f4b821d Mon Sep 17 00:00:00 2001 From: AJAmit17 Date: Tue, 3 Mar 2026 19:11:10 +0530 Subject: [PATCH 4/6] feat: add LangChain adapter to README documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 72236d0..120161d 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ Adapters are used by both `export` and `run`. Available adapters: | `git` | Git-native execution (run only) | | `openclaw` | OpenClaw format | | `nanobot` | Nanobot format | +| `langchain` | LangChain agent Python code | ```bash # Export to system prompt From 1a3764e9ddeaac830f831b1553e56d009576c602 Mon Sep 17 00:00:00 2001 From: AJAmit17 <1NH22CD011@newhorizonindia.edu> Date: Wed, 4 Mar 2026 17:14:18 +0530 Subject: [PATCH 5/6] feat: pass prompt option to runWithLangChain function for enhanced agent interaction --- src/commands/run.ts | 2 +- src/runners/langchain.ts | 94 ++++++++++++++++++++++++++++++++++------ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/commands/run.ts b/src/commands/run.ts index 77234d6..015ff34 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -114,7 +114,7 @@ export const runCommand = new Command('run') await runWithGitHub(agentDir, manifest, { prompt: options.prompt }); break; case 'langchain': - runWithLangChain(agentDir, manifest); + runWithLangChain(agentDir, manifest, { prompt: options.prompt }); break; case 'git': if (!options.repo) { diff --git a/src/runners/langchain.ts b/src/runners/langchain.ts index fb89e05..2e18d3c 100644 --- a/src/runners/langchain.ts +++ b/src/runners/langchain.ts @@ -1,44 +1,110 @@ -import { writeFileSync, unlinkSync } from 'node:fs'; +import { writeFileSync, unlinkSync, existsSync } from 'node:fs'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; +import { tmpdir, homedir, platform } from 'node:os'; import { spawnSync } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { exportToLangChain, detectProvider } from '../adapters/langchain.js'; import { AgentManifest } from '../utils/loader.js'; import { error, info } from '../utils/format.js'; -export function runWithLangChain(agentDir: string, _manifest: AgentManifest): void { +const IS_WINDOWS = platform() === 'win32'; + +/** Paths inside a venv differ between Windows and Unix. */ +function venvPython(venvDir: string): string { + return IS_WINDOWS + ? join(venvDir, 'Scripts', 'python.exe') + : join(venvDir, 'bin', 'python'); +} + +function venvPip(venvDir: string): string { + return IS_WINDOWS + ? join(venvDir, 'Scripts', 'pip.exe') + : join(venvDir, 'bin', 'pip'); +} + +/** Find a system Python 3 to bootstrap the venv. */ +function findSystemPython(): string | null { + for (const cmd of ['python3', 'python']) { + const r = spawnSync(cmd, ['--version'], { stdio: 'pipe' }); + if (!r.error && r.status === 0) { + // Reject the Windows Store stub (outputs to stderr, version string absent from stdout) + const out = (r.stdout?.toString() ?? '') + (r.stderr?.toString() ?? ''); + if (out.includes('Python 3')) return cmd; + } + } + return null; +} + +export function runWithLangChain(agentDir: string, _manifest: AgentManifest, options: { prompt?: string } = {}): void { const model = _manifest.model?.preferred ?? 'gpt-4o'; const providerInfo = detectProvider(model); // Check the appropriate API key env var - const apiKey = process.env[providerInfo.envVar]; - if (!apiKey) { + if (!process.env[providerInfo.envVar]) { error(`${providerInfo.envVar} environment variable is not set`); - info(`Set it with: export ${providerInfo.envVar}="your-key-here"`); + info(`Set it with: ${IS_WINDOWS ? '$env:' : 'export '}${providerInfo.envVar}="your-key-here"`); process.exit(1); } + // Persistent venv at ~/.gitagent/gitagent-env — reused across runs + const venvDir = join(homedir(), '.gitagent', 'gitagent-env'); + const packages = ['langchain', 'langchain-core', providerInfo.pipPackage]; + + // --- Step 1: create venv if it doesn't exist --- + if (!existsSync(venvPython(venvDir))) { + info(`Creating Python virtual environment at ${venvDir} ...`); + const sysPython = findSystemPython(); + if (!sysPython) { + error('Python 3 not found. Please install Python 3 and try again.'); + process.exit(1); + } + const create = spawnSync(sysPython, ['-m', 'venv', venvDir], { stdio: 'inherit' }); + if (create.status !== 0) { + error('Failed to create virtual environment.'); + process.exit(1); + } + } + + // --- Step 2: install packages if any are missing --- + const checkImport = spawnSync( + venvPython(venvDir), + ['-c', `import langchain; import ${providerInfo.pipPackage.replace(/-/g, '_')}`], + { stdio: 'pipe' }, + ); + + if (checkImport.status !== 0) { + info(`Installing packages: ${packages.join(' ')} ...`); + const install = spawnSync( + venvPip(venvDir), + ['install', '--quiet', '--upgrade', ...packages], + { stdio: 'inherit' }, + ); + if (install.status !== 0) { + error('Failed to install required packages.'); + info(`Try manually: ${venvPip(venvDir)} install ${packages.join(' ')}`); + process.exit(1); + } + } + + // --- Step 3: write + run script --- const script = exportToLangChain(agentDir); const tmpFile = join(tmpdir(), `gitagent-langchain-${randomBytes(4).toString('hex')}.py`); - writeFileSync(tmpFile, script, 'utf-8'); - const pipHint = `langchain ${providerInfo.pipPackage}`; - info(`Running LangChain agent from "${agentDir}"...`); - info(`Requires: pip install ${pipHint}`); + info(`Running LangChain agent from "${agentDir}" ...`); + + // Pass prompt as a CLI arg so the script receives it via sys.argv + const scriptArgs = options.prompt ? [tmpFile, options.prompt] : [tmpFile]; try { - const result = spawnSync('python3', [tmpFile], { + const result = spawnSync(venvPython(venvDir), scriptArgs, { stdio: 'inherit', cwd: agentDir, env: { ...process.env }, }); if (result.error) { - error(`Failed to run Python: ${result.error.message}`); - info('Make sure python3 is installed and langchain packages are available:'); - info(` pip install ${pipHint}`); + error(`Failed to run script: ${result.error.message}`); process.exit(1); } From db0872bea2cd283fee00c3fccd119dff1ebe6ab8 Mon Sep 17 00:00:00 2001 From: AJAmit17 <1NH22CD011@newhorizonindia.edu> Date: Wed, 4 Mar 2026 17:46:03 +0530 Subject: [PATCH 6/6] feat: enhance provider detection and error handling in LangChain integration --- src/adapters/langchain.ts | 29 ++++++++++++++++++++--------- src/runners/langchain.ts | 9 +++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/adapters/langchain.ts b/src/adapters/langchain.ts index a0c90cc..6dee812 100644 --- a/src/adapters/langchain.ts +++ b/src/adapters/langchain.ts @@ -13,16 +13,15 @@ interface ProviderInfo { envVar: string; } -function detectProvider(model: string): ProviderInfo { +/** Supported providers. Returns null for any unsupported model. */ +function detectProvider(model: string): ProviderInfo | null { const m = model.toLowerCase(); - if (m.startsWith('claude')) return { provider: 'anthropic', pipPackage: 'langchain-anthropic', envVar: 'ANTHROPIC_API_KEY' }; - if (m.startsWith('gemini')) return { provider: 'google_genai', pipPackage: 'langchain-google-genai', envVar: 'GOOGLE_API_KEY' }; - if (m.startsWith('grok')) return { provider: 'xai', pipPackage: 'langchain-xai', envVar: 'XAI_API_KEY' }; - if (m.startsWith('mistral')) return { provider: 'mistralai', pipPackage: 'langchain-mistralai', envVar: 'MISTRAL_API_KEY' }; - if (m.startsWith('deepseek')) return { provider: 'deepseek', pipPackage: 'langchain-deepseek', envVar: 'DEEPSEEK_API_KEY' }; - if (m.startsWith('command')) return { provider: 'cohere', pipPackage: 'langchain-cohere', envVar: 'COHERE_API_KEY' }; - // Default: OpenAI (covers gpt-*, o1-*, o3-*, etc.) - return { provider: 'openai', pipPackage: 'langchain-openai', envVar: 'OPENAI_API_KEY' }; + // Anthropic — claude-* + if (m.startsWith('claude')) return { provider: 'anthropic', pipPackage: 'langchain-anthropic', envVar: 'ANTHROPIC_API_KEY' }; + // OpenAI — gpt-*, o1-*, o2-*, o3-*, o4-* + if (m.startsWith('gpt') || /^o\d/.test(m)) return { provider: 'openai', pipPackage: 'langchain-openai', envVar: 'OPENAI_API_KEY' }; + // Unsupported model + return null; } // Make detectProvider available for tests @@ -52,11 +51,23 @@ export function exportToLangChain(dir: string): string { // Collect all providers needed (main + sub-agents) const mainProvider = detectProvider(model); + if (!mainProvider) { + throw new Error( + `Model "${model}" is not supported by the LangChain adapter.\n` + + 'gitagent with LangChain currently supports OpenAI (gpt-*, o1-*, o3-*, …) and Anthropic (claude-*) only.' + ); + } const allPipPackages = new Set([mainProvider.pipPackage]); const allEnvVars = new Set([mainProvider.envVar]); for (const sub of subAgents) { const subProv = detectProvider(sub.model ?? model); + if (!subProv) { + throw new Error( + `Sub-agent model "${sub.model ?? model}" is not supported by the LangChain adapter.\n` + + 'gitagent with LangChain currently supports OpenAI (gpt-*, o1-*, o3-*, …) and Anthropic (claude-*) only.' + ); + } allPipPackages.add(subProv.pipPackage); allEnvVars.add(subProv.envVar); } diff --git a/src/runners/langchain.ts b/src/runners/langchain.ts index 2e18d3c..2e35b43 100644 --- a/src/runners/langchain.ts +++ b/src/runners/langchain.ts @@ -39,6 +39,15 @@ export function runWithLangChain(agentDir: string, _manifest: AgentManifest, opt const model = _manifest.model?.preferred ?? 'gpt-4o'; const providerInfo = detectProvider(model); + // Unsupported model — tell the user clearly + if (!providerInfo) { + error(`Model "${model}" is not supported by the LangChain adapter.`); + info('gitagent with LangChain currently supports:'); + info(' • OpenAI — gpt-4o, gpt-4, o1-mini, o3-mini, …'); + info(' • Anthropic — claude-3-5-sonnet, claude-3-opus, …'); + process.exit(1); + } + // Check the appropriate API key env var if (!process.env[providerInfo.envVar]) { error(`${providerInfo.envVar} environment variable is not set`);