agent-factory: generate agent auto-parse-email-content-and-extrac

This commit is contained in:
2026-04-07 20:54:30 +00:00
parent 9ac8be2c02
commit 58e98ad062
18 changed files with 549 additions and 0 deletions

1
.image-version Normal file
View File

@@ -0,0 +1 @@
1.0.0

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["uvicorn", "app.agent:app", "--host", "0.0.0.0", "--port", "8080"]

0
app/__init__.py Normal file
View File

148
app/agent.py Normal file
View File

@@ -0,0 +1,148 @@
"""
AutoParseEmailContentAndExtrac Agent — auto-generated by Agent Factory.
"""
import asyncio
import logging
import os
import click
import httpx
import uvicorn
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from langchain_core.rate_limiters import InMemoryRateLimiter
from langchain_openai import AzureChatOpenAI
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from app.config import (
AGENT_SELF_URL,
AZURE_OPENAI_API_KEY,
AZURE_OPENAI_API_VERSION,
AZURE_OPENAI_DEPLOYMENT,
AZURE_OPENAI_ENDPOINT,
LOG_LEVEL,
REGISTRY_URL,
)
from app.skills import AGENT_CONFIG, AUTO_PARSE_EMAIL_CONTENT_AND_EXTRAC_SKILLS
from app.workflows.auto_parse_email_content_and_extrac_workflow import create_auto_parse_email_content_and_extrac_workflow
load_dotenv()
# ── Logging ──────────────────────────────────────────────────────────────
logging.basicConfig(
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
format="%(asctime)s %(name)s %(levelname)s %(message)s",
)
logger = logging.getLogger(__name__)
# ── LLM ──────────────────────────────────────────────────────────────────
rate_limiter = InMemoryRateLimiter(
requests_per_second=10 / 60,
check_every_n_seconds=0.1,
max_bucket_size=10,
)
llm = AzureChatOpenAI(
temperature=0,
azure_deployment=AZURE_OPENAI_DEPLOYMENT,
api_version=AZURE_OPENAI_API_VERSION,
azure_endpoint=AZURE_OPENAI_ENDPOINT or "",
api_key=AZURE_OPENAI_API_KEY or "",
max_retries=5,
timeout=120,
rate_limiter=rate_limiter,
)
workflow = create_auto_parse_email_content_and_extrac_workflow(llm)
# ── Endpoints ────────────────────────────────────────────────────────────
async def health_check(request: Request) -> JSONResponse:
return JSONResponse({"status": "healthy", "agent": 'AutoParseEmailContentAndExtrac'})
async def agent_manifest(request: Request) -> JSONResponse:
"""GET /.well-known/agent.json"""
return JSONResponse({
"name": AGENT_CONFIG["name"],
"version": AGENT_CONFIG["version"],
"description": AGENT_CONFIG["description"],
"url": "/",
"skills": [
{
"id": s.id,
"name": s.name,
"description": s.description,
"tags": s.tags,
"inputSchema": AGENT_CONFIG.get("input_schema"),
"outputSchema": AGENT_CONFIG.get("output_schema"),
} for s in AUTO_PARSE_EMAIL_CONTENT_AND_EXTRAC_SKILLS
],
"capabilities": AGENT_CONFIG["capabilities"],
})
async def process_endpoint(request: Request) -> JSONResponse:
"""POST /process — run the agent workflow."""
try:
body = await request.json()
result = await workflow.ainvoke(body)
return JSONResponse(result)
except Exception as exc:
logger.error("Processing failed: %s", exc, exc_info=True)
return JSONResponse({"error": str(exc)}, status_code=500)
# ── Self-registration ────────────────────────────────────────────────────
async def _register_with_registry():
if not REGISTRY_URL:
logger.info("REGISTRY_URL not set — skipping self-registration")
return
await asyncio.sleep(2)
url = f"{REGISTRY_URL.rstrip('/')}/agents/register-url"
for attempt in range(3):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(url, json={"endpoint": AGENT_SELF_URL})
if resp.status_code in (200, 201):
logger.info("Self-registered with agent-registry at %s", REGISTRY_URL)
return
logger.warning("Registration attempt %d: HTTP %d", attempt + 1, resp.status_code)
except Exception as exc:
logger.warning("Registration attempt %d failed: %s", attempt + 1, exc)
await asyncio.sleep(5)
logger.error("Failed to self-register after 3 attempts")
@asynccontextmanager
async def lifespan(app):
task = asyncio.create_task(_register_with_registry())
yield
task.cancel()
app = Starlette(
routes=[
Route("/health", methods=["GET"], endpoint=health_check),
Route("/.well-known/agent.json", methods=["GET"], endpoint=agent_manifest),
Route("/process", methods=["POST"], endpoint=process_endpoint),
],
lifespan=lifespan,
)
@click.command()
@click.option("--host", default="0.0.0.0")
@click.option("--port", default=8080, type=int)
def main(host: str, port: int):
uvicorn.run(app, host=host, port=port, log_level=LOG_LEVEL.lower())
if __name__ == "__main__":
main()

21
app/config.py Normal file
View File

@@ -0,0 +1,21 @@
"""
Configuration for the AutoParseEmailContentAndExtrac agent.
"""
import os
# ── Azure OpenAI ─────────────────────────────────────────────────────────
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview")
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o")
# ── Agent Registry ───────────────────────────────────────────────────────
REGISTRY_URL = os.getenv(
"REGISTRY_URL", "http://agent-gateway.agents.svc.cluster.local"
)
AGENT_SELF_URL = os.getenv(
"AGENT_SELF_URL", "http://auto-parse-email-content-and-extrac.agents.svc.cluster.local"
)
# ── Logging ──────────────────────────────────────────────────────────────
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

0
app/nodes/__init__.py Normal file
View File

41
app/nodes/core_node.py Normal file
View File

@@ -0,0 +1,41 @@
"""
This module defines a LangGraph node function that parses raw email content
and extracts the sender's email address, the email subject, and the email body.
The function uses an LLM to perform the extraction and returns the parsed
information in a structured format.
"""
async def process(state: dict) -> dict:
from app.agent import llm
from langchain_core.messages import SystemMessage, HumanMessage
try:
email_raw = state.get("email_raw", "")
if not email_raw:
return {"error": "Missing 'email_raw' input", "phase": "failed"}
messages = [
SystemMessage(content="You are an expert in parsing email content. Extract the sender's email address, the subject, and the body from the provided raw email text."),
HumanMessage(content=email_raw),
]
response = await llm.ainvoke(messages)
# Assuming the LLM returns a structured response in the format:
# "Sender: <email>\nSubject: <subject>\nBody: <body>"
try:
lines = response.content.split("\n")
sender_email = next((line.split(": ", 1)[1] for line in lines if line.startswith("Sender:")), "").strip()
email_subject = next((line.split(": ", 1)[1] for line in lines if line.startswith("Subject:")), "").strip()
email_body = next((line.split(": ", 1)[1] for line in lines if line.startswith("Body:")), "").strip()
return {
"sender_email": sender_email,
"email_subject": email_subject,
"email_body": email_body,
"phase": "complete"
}
except Exception as parse_exc:
return {"error": f"Failed to parse LLM response: {str(parse_exc)}", "phase": "failed"}
except Exception as exc:
return {"error": str(exc), "phase": "failed"}

29
app/skills.py Normal file
View File

@@ -0,0 +1,29 @@
"""
A2A skill declarations for AutoParseEmailContentAndExtrac.
"""
from a2a.types import AgentSkill
AUTO_PARSE_EMAIL_CONTENT_AND_EXTRAC_SKILLS = [
AgentSkill(
id="auto_parse_email_content_and_extrac_skill",
name="AutoParseEmailContentAndExtrac",
description="Parse email content and extract sender, subject, and body",
tags=["auto-generated"],
examples=[],
),
]
AGENT_CONFIG = {
"name": "AutoParseEmailContentAndExtrac",
"description": "Extract relevant fields from incoming support email",
"version": "1.0.0",
"framework": "LangGraph + Starlette",
"capabilities": {
"streaming": False,
"async": True,
},
"input_schema": {"email_raw": "string"},
"output_schema": {"sender_email": "string", "email_subject": "string", "email_body": "string"},
}

0
app/states/__init__.py Normal file
View File

View File

@@ -0,0 +1,17 @@
"""
State definitions for AutoParseEmailContentAndExtrac agent.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from langgraph.graph import MessagesState
class AutoParseEmailContentAndExtracState(MessagesState):
"""Workflow state for AutoParseEmailContentAndExtrac."""
# ── Input fields ─────────────────────────────────────────────────────
pass
# ── Output fields ────────────────────────────────────────────────────
pass
# ── Internal ─────────────────────────────────────────────────────────
error: Optional[str]
phase: str

View File

View File

@@ -0,0 +1,19 @@
"""
LangGraph workflow for AutoParseEmailContentAndExtrac agent.
"""
from langgraph.graph import StateGraph, END
from app.states.auto_parse_email_content_and_extrac_state import AutoParseEmailContentAndExtracState
from app.nodes.core_node import process
def create_auto_parse_email_content_and_extrac_workflow(llm):
"""Build and compile the AutoParseEmailContentAndExtrac workflow graph."""
graph = StateGraph(AutoParseEmailContentAndExtracState)
graph.add_node("process", process)
graph.set_entry_point("process")
graph.add_edge("process", END)
return graph.compile()

13
k8s/configmap.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: auto-parse-email-content-and-extrac-config
namespace: agents
data:
REGISTRY_URL: "http://agent-gateway.agents.svc.cluster.local"
AGENT_SELF_URL: "http://auto-parse-email-content-and-extrac.agents.svc.cluster.local"
AZURE_OPENAI_DEPLOYMENT: "gpt-4o"
AZURE_OPENAI_API_VERSION: "2024-08-01-preview"
LOG_LEVEL: "INFO"
OTEL_SERVICE_NAME: "auto-parse-email-content-and-extrac"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring.svc.cluster.local:4318"

81
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,81 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: auto-parse-email-content-and-extrac
namespace: agents
labels:
app: auto-parse-email-content-and-extrac
component: agent
generated-by: agent-factory
spec:
replicas: 1
selector:
matchLabels:
app: auto-parse-email-content-and-extrac
template:
metadata:
labels:
app: auto-parse-email-content-and-extrac
azure.workload.identity/use: "true"
annotations:
instrumentation.opentelemetry.io/inject-python: "monitoring/otel-instrumentation"
spec:
serviceAccountName: agents-sa
containers:
- name: auto-parse-email-content-and-extrac
image: bstagecjotdevacr.azurecr.io/auto-parse-email-content-and-extrac:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: auto-parse-email-content-and-extrac-config
env:
- name: AZURE_OPENAI_ENDPOINT
valueFrom:
secretKeyRef:
name: agents-kv-sync
key: azure-openai-endpoint
- name: AZURE_OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: agents-kv-sync
key: azure-openai-api-key
- name: OTEL_SERVICE_NAME
value: "auto-parse-email-content-and-extrac"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.namespace=agents,deployment.environment=production"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 5
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: agents-kv-spc

16
k8s/service.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: auto-parse-email-content-and-extrac
namespace: agents
labels:
app: auto-parse-email-content-and-extrac
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: auto-parse-email-content-and-extrac

18
requirements.txt Normal file
View File

@@ -0,0 +1,18 @@
# Auto-generated by Agent Factory
uvicorn[standard]==0.32.1
starlette>=0.28.0
pydantic>=2.11.3
python-dotenv==1.0.1
httpx>=0.28.1
click>=8.1.8
# A2A Protocol
a2a-sdk[http-server]>=0.3.0
# LangChain & LangGraph
langchain>=1.2.10
langchain-openai>=0.2.12
langchain-core>=1.2.11
langgraph>=0.2.59
langgraph-checkpoint>=2.0.6
openai>=1.109.1

54
scripts/deploy.sh Normal file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# deploy.sh — Build auto-parse-email-content-and-extrac image in ACR and deploy to AKS
set -e
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACR_NAME="bstagecjotdevacr"
APP_NAME="auto-parse-email-content-and-extrac"
NAMESPACE="agents"
VERSION_FILE="${SCRIPT_DIR}/../.image-version"
TAG=""
while [[ "$#" -gt 0 ]]; do
case $1 in
--tag) TAG="$2"; shift ;;
esac
shift
done
if [ -z "$TAG" ]; then
if [ -f "$VERSION_FILE" ]; then
CURRENT_VERSION=$(cat "$VERSION_FILE")
else
CURRENT_VERSION="1.0.0"
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
PATCH=$((PATCH + 1))
TAG="${MAJOR}.${MINOR}.${PATCH}"
echo "$TAG" > "$VERSION_FILE"
fi
IMAGE="${ACR_NAME}.azurecr.io/${APP_NAME}:${TAG}"
echo "======================================================================"
echo " auto-parse-email-content-and-extrac — Build & Deploy"
echo " Image: ${IMAGE}"
echo "======================================================================"
az acr build --registry ${ACR_NAME} \
--image ${APP_NAME}:${TAG} \
--image ${APP_NAME}:latest \
"${SCRIPT_DIR}/.."
kubectl apply -f "${SCRIPT_DIR}/../k8s/configmap.yaml" 2>/dev/null || true
kubectl apply -f "${SCRIPT_DIR}/../k8s/deployment.yaml"
kubectl apply -f "${SCRIPT_DIR}/../k8s/service.yaml" 2>/dev/null || true
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${IMAGE} \
-n ${NAMESPACE}
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE} --timeout=120s
echo "✓ Done. Image: ${IMAGE}"
echo " Port forward: kubectl port-forward -n ${NAMESPACE} deployment/${APP_NAME} 8080:8080"

66
scripts/full-deploy.sh Normal file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
# full-deploy.sh — Preflight + build + deploy auto-parse-email-content-and-extrac to AKS
set -e
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACR_NAME="bstagecjotdevacr"
APP_NAME="auto-parse-email-content-and-extrac"
NAMESPACE="agents"
VERSION_FILE="${SCRIPT_DIR}/../.image-version"
TAG=""
while [[ "$#" -gt 0 ]]; do
case $1 in
--tag) TAG="$2"; shift ;;
esac
shift
done
print_success() { echo -e "${GREEN}$1${NC}"; }
print_error() { echo -e "${RED}$1${NC}"; exit 1; }
command -v kubectl &>/dev/null || print_error "kubectl not found."
command -v az &>/dev/null || print_error "Azure CLI not found."
az account show &>/dev/null || print_error "Not logged in to Azure."
kubectl cluster-info &>/dev/null || print_error "Cannot connect to cluster."
CLUSTER=$(kubectl config current-context)
print_success "Connected to cluster: ${CLUSTER}"
if [ -z "$TAG" ]; then
if [ -f "$VERSION_FILE" ]; then
CURRENT_VERSION=$(cat "$VERSION_FILE")
else
CURRENT_VERSION="1.0.0"
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
PATCH=$((PATCH + 1))
TAG="${MAJOR}.${MINOR}.${PATCH}"
echo "$TAG" > "$VERSION_FILE"
fi
IMAGE="${ACR_NAME}.azurecr.io/${APP_NAME}:${TAG}"
print_success "Image tag: ${TAG}"
az acr build --registry ${ACR_NAME} \
--image ${APP_NAME}:${TAG} \
--image ${APP_NAME}:latest \
"${SCRIPT_DIR}/.." || print_error "ACR build failed."
print_success "Image built: ${IMAGE}"
kubectl apply -f "${SCRIPT_DIR}/../k8s/configmap.yaml" 2>/dev/null || true
kubectl apply -f "${SCRIPT_DIR}/../k8s/deployment.yaml" || print_error "Failed to apply deployment."
kubectl apply -f "${SCRIPT_DIR}/../k8s/service.yaml" || print_error "Failed to apply service."
print_success "Manifests applied"
kubectl set image deployment/${APP_NAME} ${APP_NAME}=${IMAGE} -n ${NAMESPACE}
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE} --timeout=180s || \
print_error "Rollout failed."
print_success "Rollout complete"
echo ""
echo -e "${GREEN} auto-parse-email-content-and-extrac deployed: ${IMAGE}${NC}"
echo " Port forward: kubectl port-forward -n ${NAMESPACE} deployment/${APP_NAME} 8080:8080"