commit 4886fb323652cc03333ed59fc3186ff93d9452bc Author: David Xu Date: Sat Jun 7 18:24:34 2025 -0700 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c4ff8b --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY= +LANGSMITH_API_KEY= diff --git a/.github/workflows/new-deployment.yml b/.github/workflows/new-deployment.yml new file mode 100644 index 0000000..197067f --- /dev/null +++ b/.github/workflows/new-deployment.yml @@ -0,0 +1,170 @@ +name: Create New Deployment + +# Trigger the workflow on push or pull request merge to main +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + types: [ closed ] + +# Set environment variables +env: + PYTHON_VERSION: '3.11' + REGISTRY: docker.io # Change to ghcr.io for GitHub Container Registry + IMAGE_NAME: davidxu33/control-plane-api-demo # Replace YOUR_DOCKERHUB_USERNAME with your actual Docker Hub username + +jobs: + # Job 1: Run custom logic and tests + run-custom-logic: + if: false && (github.ref == 'refs/heads/main' || (github.event.pull_request.merged == true)) + runs-on: ubuntu-latest + + outputs: + should-deploy: ${{ steps.validation.outputs.deploy }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then + pip install -r requirements.txt + fi + + - name: Run validation and tests + id: validation + run: | + echo "✅ Skipping validation tests - proceeding with deployment" + # Set output to indicate that deployment should proceed + echo "deploy=true" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Job 2: Build and push Docker image + build-and-push: + needs: run-custom-logic + if: needs.run-custom-logic.outputs.should-deploy == 'true' + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write # Required for pushing to GitHub Container Registry + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Step 1: Set up Docker Buildx (advanced Docker build features) + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Step 2: Log in to Docker registry + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Step 3: Extract metadata for Docker tags and labels + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Branch-based tags + type=ref,event=branch + # SHA-based tags + type=sha,prefix={{branch}}- + # Latest tag for main branch + type=raw,value=latest,enable={{is_default_branch}} + # Semantic versioning (if you use git tags) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + labels: | + org.opencontainers.image.title=LangGraph AI Agent + org.opencontainers.image.description=AI Agent built with LangGraph + org.opencontainers.image.vendor=YourCompany + + # Step 4: Build and push Docker image + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile # Path to your Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 # Multi-platform build + cache-from: type=gha # Use GitHub Actions cache + cache-to: type=gha,mode=max + build-args: | + PYTHON_VERSION=${{ env.PYTHON_VERSION }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + + # Step 5: Output image details + - name: Output image details + run: | + echo "🐳 Docker image built and pushed successfully!" + echo "📦 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}" + echo "📋 Digest: ${{ steps.build.outputs.digest }}" + + # Job 3: Post-deployment actions (optional) + post-deployment: + needs: [run-custom-logic, build-and-push] + if: success() + runs-on: ubuntu-latest + + steps: + - name: Deploy notification + run: | + echo "✅ Deployment completed successfully!" + echo "🚀 New image available at: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" + + # Deploy to LangChain hosted platform + - name: Trigger LangChain deployment + run: | + echo "🚀 Triggering deployment to LangChain hosted platform..." + + response=$(curl -s -w "HTTPSTATUS:%{http_code}" \ + https://gtm.smith.langchain.dev/api-host/v1/projects \ + --request POST \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ${{ secrets.LANGSMITH_API_KEY }}' \ + --data '{ + "name": "multi-agent-${{ github.sha }}", + "lc_hosted": false, + "env_vars": [{"name": "OPENAI_API_KEY", "value": "${{ secrets.OPENAI_API_KEY }}", "type": "secret"}], + "deployment_type": "dev", + "shareable": false, + "image_path": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest", + "build_on_push": false + }') + + # Extract HTTP status and response body + http_status=$(echo $response | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + response_body=$(echo $response | sed -e 's/HTTPSTATUS\:.*//g') + + # Check if deployment was successful + if [ $http_status -eq 200 ] || [ $http_status -eq 201 ]; then + echo "✅ LangChain deployment triggered successfully!" + echo "📋 Response: $response_body" + else + echo "❌ LangChain deployment failed with status: $http_status" + echo "📋 Response: $response_body" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/new-revision.yml b/.github/workflows/new-revision.yml new file mode 100644 index 0000000..ce11b95 --- /dev/null +++ b/.github/workflows/new-revision.yml @@ -0,0 +1,161 @@ +name: Create New Revision + +# Trigger the workflow on push to main +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + types: [ closed ] + +# Set environment variables +env: + PYTHON_VERSION: '3.11' + REGISTRY: docker.io + IMAGE_NAME: davidxu33/control-plane-api-demo + PROJECT_ID: 'd6cee583-a32a-4342-a748-3dfc857ba964' # Replace with your actual project ID + +jobs: + # Job 1: Run validation + validate: + if: github.ref == 'refs/heads/main' || (github.event.pull_request.merged == true) + runs-on: ubuntu-latest + + outputs: + should-deploy: ${{ steps.validation.outputs.deploy }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then + pip install -r requirements.txt + fi + + - name: Run validation + id: validation + run: | + echo "✅ Validation passed - proceeding with revision update" + echo "deploy=true" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Job 2: Build and push Docker image + build-and-push: + needs: validate + if: needs.validate.outputs.should-deploy == 'true' + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + labels: | + org.opencontainers.image.title=LangGraph AI Agent + org.opencontainers.image.description=AI Agent built with LangGraph + org.opencontainers.image.vendor=YourCompany + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + PYTHON_VERSION=${{ env.PYTHON_VERSION }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + + - name: Output image details + run: | + echo "🐳 Docker image built and pushed successfully!" + echo "📦 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}" + + # Job 3: Update project revision + update-revision: + needs: [validate, build-and-push] + if: success() + runs-on: ubuntu-latest + + steps: + - name: Update notification + run: | + echo "🔄 Updating project revision..." + echo "📦 Project ID: ${{ env.PROJECT_ID }}" + echo "🚀 New image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" + + - name: Update LangChain project revision + run: | + echo "🔄 Triggering revision update for project ${{ env.PROJECT_ID }}..." + + response=$(curl -s -w "HTTPSTATUS:%{http_code}" \ + https://gtm.smith.langchain.dev/api-host/v1/projects/${{ env.PROJECT_ID }}/revisions \ + --request POST \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: ${{ secrets.LANGSMITH_API_KEY }}' \ + --data '{ + "image_path": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest", + "env_vars": [{"name": "OPENAI_API_KEY", "value": "${{ secrets.OPENAI_API_KEY }}", "type": "secret"}] + }') + + # Extract HTTP status and response body + http_status=$(echo $response | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + response_body=$(echo $response | sed -e 's/HTTPSTATUS\:.*//g') + + # Check if revision update was successful + if [ $http_status -eq 200 ] || [ $http_status -eq 201 ]; then + echo "✅ Project revision updated successfully!" + echo "📋 Response: $response_body" + else + echo "❌ Project revision update failed with status: $http_status" + echo "📋 Response: $response_body" + exit 1 + fi + + - name: Success notification + run: | + echo "🎉 Revision update completed successfully!" + echo "📦 Project: ${{ env.PROJECT_ID }}" + echo "🚀 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..57d51f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM langchain/langgraph-api:3.11 + + + +# -- Installing local requirements -- +ADD requirements.txt /deps/__outer_control-plane-api-demo/src/requirements.txt +RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -r /deps/__outer_control-plane-api-demo/src/requirements.txt +# -- End of local requirements install -- + +# -- Adding non-package dependency control-plane-api-demo -- +ADD . /deps/__outer_control-plane-api-demo/src +RUN set -ex && \ + for line in '[project]' \ + 'name = "control-plane-api-demo"' \ + 'version = "0.1"' \ + '[tool.setuptools.package-data]' \ + '"*" = ["**/*"]'; do \ + echo "$line" >> /deps/__outer_control-plane-api-demo/pyproject.toml; \ + done +# -- End of non-package dependency control-plane-api-demo -- + +# -- Installing all local dependencies -- +RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- +ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_control-plane-api-demo/src/agent.py:graph"}' + + + +# -- Ensure user deps didn't inadvertently overwrite langgraph-api +RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py +RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir --no-deps -e /api +# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api -- +# -- Removing pip from the final image ~<:===~~~ -- +RUN pip uninstall -y pip setuptools wheel && rm -rf /usr/local/lib/python*/site-packages/pip* /usr/local/lib/python*/site-packages/setuptools* /usr/local/lib/python*/site-packages/wheel* && find /usr/local/bin -name "pip*" -delete +# -- End of pip removal -- + +WORKDIR /deps/__outer_control-plane-api-demo/src \ No newline at end of file diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..c1a899d --- /dev/null +++ b/agent.py @@ -0,0 +1,28 @@ +from typing import Annotated + +from typing_extensions import TypedDict + +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages + +class State(TypedDict): + # Messages have the type "list". The `add_messages` function + # in the annotation defines how this state key should be updated + # (in this case, it appends messages to the list, rather than overwriting them) + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(model="o3-mini") + +def chatbot(state: State): + return {"messages": [llm.invoke(state["messages"])]} + +graph_builder.add_node("chatbot", chatbot) + +graph_builder.add_edge(START, "chatbot") +graph_builder.add_edge("chatbot", END) + +graph = graph_builder.compile() \ No newline at end of file diff --git a/langgraph.json b/langgraph.json new file mode 100644 index 0000000..beea624 --- /dev/null +++ b/langgraph.json @@ -0,0 +1,8 @@ +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.py:graph" + }, + "env": ".env" + } + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d07931d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +langgraph +langchain-openai +typing_extensions \ No newline at end of file