init commit

This commit is contained in:
bracesproul 2025-05-21 14:47:56 -07:00
commit e2415aee3f
23 changed files with 6359 additions and 0 deletions

0
.codespellignore Normal file
View file

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
.next
.git
.env

14
.env.example Normal file
View file

@ -0,0 +1,14 @@
# Required
OPENAI_API_KEY=""
# GOOGLE_API_KEY=""
# ANTHROPIC_API_KEY=""
MCP_URL=""
# Optional, but recommended for best in class tracing and observability.
# LANGSMITH_PROJECT="default"
# LANGSMITH_API_KEY=""
# LANGSMITH_TRACING_V2=true
# Optional
# FINANCIAL_DATASETS_API_KEY=""

68
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,68 @@
# Run formatting on all PRs
name: CI
on:
push:
branches: ["main"]
pull_request:
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
# If another push to the same PR or branch happens while this workflow is still running,
# cancel the earlier run in favor of the next run.
#
# There's no point in testing an outdated version of the code. GitHub only allows
# a limited number of job runners to be active at the same time, so it's better to cancel
# pointless jobs early so that more useful jobs can run sooner.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
format:
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: "yarn"
- name: Install dependencies
run: yarn install
- name: Check formatting
run: yarn format:check
lint:
name: Check linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: "yarn"
- name: Install dependencies
run: yarn install
- name: Check linting
run: yarn lint
readme-spelling:
name: Check README spelling
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: codespell-project/actions-codespell@v2
with:
ignore_words_file: .codespellignore
path: README.md
check-spelling:
name: Check code spelling
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: codespell-project/actions-codespell@v2
with:
ignore_words_file: .codespellignore
path: src

28
.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# LangGraph API
.langgraph_api
.env

4
.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Brace Sproul
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View file

@ -0,0 +1 @@
# OpenCodex

77
eslint.config.js Normal file
View file

@ -0,0 +1,77 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import eslintImport from "eslint-plugin-import";
import eslintNoInstanceof from "eslint-plugin-no-instanceof";
export default tseslint.config(
tseslint.configs.recommendedTypeChecked,
js.configs.recommended,
{ ignores: ["dist", "eslint.config.js"] },
{
files: ["**/*.ts"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
globals: {
...globals.node,
...globals.browser,
},
},
plugins: {
import: eslintImport,
"no-instanceof": eslintNoInstanceof,
},
rules: {
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-shadow": 0,
"@typescript-eslint/no-unsafe-assignment": 0,
"@typescript-eslint/no-unsafe-member-access": 0,
"@typescript-eslint/no-unsafe-return": 0,
"@typescript-eslint/no-unsafe-argument": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_|^UNUSED_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-explicit-any": 0,
camelcase: 0,
"class-methods-use-this": 0,
"import/extensions": [2, "ignorePackages"],
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: ["**/*.test.ts"] },
],
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"keyword-spacing": "error",
"max-classes-per-file": 0,
"max-len": 0,
"no-await-in-loop": 0,
"no-bitwise": 0,
"no-console": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-continue": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-useless-constructor": 0,
"no-return-await": 0,
"consistent-return": 0,
"no-else-return": 0,
"new-cap": ["error", { properties: false, capIsNew: false }],
},
},
);

18
jest.config.js Normal file
View file

@ -0,0 +1,18 @@
export default {
preset: "ts-jest/presets/default-esm",
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
extensionsToTreatAsEsm: [".ts"],
setupFiles: ["dotenv/config"],
passWithNoTests: true,
testTimeout: 20_000,
};

8
langgraph.json Normal file
View file

@ -0,0 +1,8 @@
{
"node_version": "20",
"graphs": {
"agent": "./src/index.ts:graph"
},
"env": ".env",
"dependencies": ["."]
}

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "open-codex",
"homepage": "https://github.com/langchain-ai/open-codex/blob/main/README.md",
"repository": {
"type": "git",
"url": "https://github.com/langchain-ai/open-codex.git"
},
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "langgraphjs dev --no-browser",
"build": "tsc",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000"
},
"dependencies": {
"@langchain/anthropic": "^0.3.20",
"@langchain/core": "^0.3.56",
"@langchain/google-genai": "^0.2.9",
"@langchain/langgraph": "^0.2.73",
"@langchain/openai": "^0.5.10",
"langchain": "^0.3.26",
"uuid": "^11.0.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.19.0",
"@langchain/langgraph-cli": "latest",
"@tsconfig/recommended": "^1.0.8",
"@types/node": "^22.13.5",
"dotenv": "^16.4.7",
"eslint": "^9.19.0",
"ts-jest": "^29.1.0",
"eslint-config-prettier": "^8.8.0",
"@types/jest": "^29.5.0",
"eslint-plugin-import": "^2.27.5",
"jest": "^29.7.0",
"eslint-plugin-no-instanceof": "^1.0.1",
"@jest/globals": "^29.7.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^3.5.2",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0"
},
"packageManager": "yarn@1.22.22"
}

92
src/index.ts Normal file
View file

@ -0,0 +1,92 @@
import { END, START, StateGraph } from "@langchain/langgraph";
import { GraphAnnotation, GraphConfiguration, GraphState } from "./types.js";
import {
generatePlan,
initialize,
generateAction,
takeAction,
rewritePlan,
summarizeSession,
} from "./nodes/index.js";
import { isAIMessage, isToolMessage } from "@langchain/core/messages";
/**
* After generating a plan, ensure there is an approved tool message.
* If there is, route to the generate-action node. Otherwise, route to the rewrite-plan node.
*/
function routeAfterPlan(state: GraphState): "generate-action" | "rewrite-plan" {
const { messages } = state;
// Search for a tool message responding to the "session_plan" tool call where the content is "approved"
const planApprovedMessage = messages.find(
(m) =>
isToolMessage(m) && m.name === "session_plan" && m.content === "approved",
);
if (planApprovedMessage) {
return "generate-action";
}
// If this does not exist, we should rewrite the plan.
return "rewrite-plan";
}
/**
* After taking action, ensure there is an AI message with tool calls.
* If there is, route to the take-action node. Otherwise, end the graph.
* We do not want to route to the summarize session node here, as the generation
* likely means the LLM is asking a question, or failed to generate an action.
*/
function takeActionOrEnd(state: GraphState): typeof END | "take-action" {
const { messages } = state;
const lastMessage = messages[messages.length - 1];
// If the message is an AI message, and it has tool calls, we should take action.
if (isAIMessage(lastMessage) && lastMessage.tool_calls?.length) {
return "take-action";
}
return END;
}
/**
* After taking action, check if there are any uncompleted plan items.
* If there are, route to the generate-action node. Otherwise, route to the summarize-session node.
* The summarize session node is the final node, and will end the graph.
*/
function generateActionOrSummarize(
state: GraphState,
): "generate-action" | "summarize-session" {
const { plan } = state;
const hasUncompletedPlanItems = !!plan.filter((item) => !item.completed)
.length;
if (hasUncompletedPlanItems) {
return "generate-action";
}
return "summarize-session";
}
const workflow = new StateGraph(GraphAnnotation, GraphConfiguration)
.addNode("initialize", initialize)
.addNode("generate-plan", generatePlan)
.addNode("rewrite-plan", rewritePlan)
.addNode("generate-action", generateAction)
.addNode("take-action", takeAction)
.addNode("summarize-session", summarizeSession)
.addEdge(START, "initialize")
.addEdge("initialize", "generate-plan")
.addConditionalEdges("generate-plan", routeAfterPlan, [
"generate-action",
"rewrite-plan",
])
.addConditionalEdges("rewrite-plan", routeAfterPlan, [
"generate-action",
"rewrite-plan",
])
.addEdge("generate-plan", "generate-action")
.addConditionalEdges("generate-action", takeActionOrEnd, ["take-action", END])
.addConditionalEdges("take-action", generateActionOrSummarize, [
"generate-action",
"summarize-session",
])
.addEdge("summarize-session", END);
export const graph = workflow.compile();
graph.name = "LangGraph ReAct MCP";

View file

@ -0,0 +1,12 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
/**
* Initializes the session. This ensures there's an active VM session, and that
* the proper credentials are provided for taking actions on GitHub.
*/
export function generateAction(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

View file

@ -0,0 +1,8 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
export function generatePlan(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

6
src/nodes/index.ts Normal file
View file

@ -0,0 +1,6 @@
export * from "./generate-plan.js";
export * from "./initialize.js";
export * from "./generate-action.js";
export * from "./take-action.js";
export * from "./rewrite-plan.js";
export * from "./summarize-session.js";

12
src/nodes/initialize.ts Normal file
View file

@ -0,0 +1,12 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
/**
* Initializes the session. This ensures there's an active VM session, and that
* the proper credentials are provided for taking actions on GitHub.
*/
export function initialize(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

12
src/nodes/rewrite-plan.ts Normal file
View file

@ -0,0 +1,12 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
/**
* Initializes the session. This ensures there's an active VM session, and that
* the proper credentials are provided for taking actions on GitHub.
*/
export function rewritePlan(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

View file

@ -0,0 +1,12 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
/**
* Initializes the session. This ensures there's an active VM session, and that
* the proper credentials are provided for taking actions on GitHub.
*/
export function summarizeSession(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

12
src/nodes/take-action.ts Normal file
View file

@ -0,0 +1,12 @@
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
/**
* Initializes the session. This ensures there's an active VM session, and that
* the proper credentials are provided for taking actions on GitHub.
*/
export function takeAction(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
throw new Error("Not implemented");
}

147
src/types.ts Normal file
View file

@ -0,0 +1,147 @@
import "@langchain/langgraph/zod";
import { z } from "zod";
import {
Annotation,
LangGraphRunnableConfig,
MessagesAnnotation,
} from "@langchain/langgraph";
type PlanItem = {
id: string;
plan: string;
completed: boolean;
};
export const GraphAnnotation = Annotation.Root({
messages: MessagesAnnotation.spec.messages,
plan: Annotation<PlanItem[]>({
reducer: (_state, update) => update,
default: () => [],
}),
});
export type GraphState = typeof GraphAnnotation.State;
export type GraphUpdate = typeof GraphAnnotation.Update;
export const MCPConfig = z.object({
/**
* The MCP server URL.
*/
url: z.string(),
/**
* The list of tools to provide to the LLM.
*/
tools: z.array(z.string()),
});
export const GraphConfiguration = z.object({
/**
* The model ID to use for the reflection generation.
* Should be in the format `provider/model_name`.
* Defaults to `anthropic/claude-3-7-sonnet-latest`.
*/
modelName: z
.string()
.optional()
.langgraph.metadata({
x_lg_ui_config: {
type: "select",
default: "anthropic/claude-3-7-sonnet-latest",
description: "The model to use in all generations",
options: [
{
label: "Claude 3.7 Sonnet",
value: "anthropic/claude-3-7-sonnet-latest",
},
{
label: "Claude 3.5 Sonnet",
value: "anthropic/claude-3-5-sonnet-latest",
},
{
label: "GPT 4o",
value: "openai/gpt-4o",
},
{
label: "GPT 4.1",
value: "openai/gpt-4.1",
},
{
label: "o3",
value: "openai/o3",
},
{
label: "o3 mini",
value: "openai/o3-mini",
},
{
label: "o4",
value: "openai/o4",
},
],
},
}),
/**
* The temperature to use for the reflection generation.
* Defaults to `0.7`.
*/
temperature: z
.number()
.optional()
.langgraph.metadata({
x_lg_ui_config: {
type: "slider",
default: 0.7,
min: 0,
max: 2,
step: 0.1,
description: "Controls randomness (0 = deterministic, 2 = creative)",
},
}),
/**
* The maximum number of tokens to generate.
* Defaults to `1000`.
*/
maxTokens: z
.number()
.optional()
.langgraph.metadata({
x_lg_ui_config: {
type: "number",
default: 4000,
min: 1,
description: "The maximum number of tokens to generate",
},
}),
systemPrompt: z
.string()
.optional()
.langgraph.metadata({
x_lg_ui_config: {
type: "textarea",
placeholder: "Enter a system prompt...",
description: "The system prompt to use in all generations",
},
}),
/**
* MCP configuration for tool selection
*/
mcpConfig: z
.lazy(() => MCPConfig)
.optional()
.langgraph.metadata({
x_lg_ui_config: {
type: "mcp",
// Add custom tools here.
// default: {
// tools: ["Math_Divide", "Math_Mod"]
// }
},
}),
});
export type GraphConfig = LangGraphRunnableConfig<
z.infer<typeof GraphConfiguration> & {
thread_id: string;
assistant_id: string;
}
>;

25
tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "DOM"],
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"noImplicitReturns": true,
"declaration": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"strictPropertyInitialization": false,
"allowJs": true,
"strict": true,
"strictFunctionTypes": false,
"outDir": "dist",
"types": ["jest", "node"],
"resolveJsonModule": true
},
"include": ["**/*.ts", "**/*.js", "jest.setup.cjs"],
"exclude": ["node_modules", "dist"]
}

5725
yarn.lock Normal file

File diff suppressed because it is too large Load diff