mirror of
https://github.com/Sea-Haven-Industries/workorder-ingest.git
synced 2026-05-18 20:20:12 +00:00
Add CI workflow and apply ruff formatting (#10)
This commit is contained in:
parent
885379f85d
commit
853fb3253f
7 changed files with 50 additions and 20 deletions
12
.github/workflows/ci.yaml
vendored
Normal file
12
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
|
||||||
|
with:
|
||||||
|
source-dirs: "lambdas cdk shared"
|
||||||
|
run-cdk-synth: true
|
||||||
|
run-sam-validate: false
|
||||||
|
|
@ -3,7 +3,9 @@ import aws_cdk as cdk
|
||||||
from stack import WorkorderIngestStack
|
from stack import WorkorderIngestStack
|
||||||
|
|
||||||
app = cdk.App()
|
app = cdk.App()
|
||||||
WorkorderIngestStack(app, "workorder-ingest",
|
WorkorderIngestStack(
|
||||||
|
app,
|
||||||
|
"workorder-ingest",
|
||||||
stack_name="WorkorderIngestStack",
|
stack_name="WorkorderIngestStack",
|
||||||
env=cdk.Environment(region="us-east-1"),
|
env=cdk.Environment(region="us-east-1"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
32
cdk/stack.py
32
cdk/stack.py
|
|
@ -12,7 +12,6 @@ from aws_cdk import (
|
||||||
aws_ses as ses,
|
aws_ses as ses,
|
||||||
aws_ses_actions as ses_actions,
|
aws_ses_actions as ses_actions,
|
||||||
aws_secretsmanager as secretsmanager,
|
aws_secretsmanager as secretsmanager,
|
||||||
aws_iam as iam,
|
|
||||||
)
|
)
|
||||||
from constructs import Construct
|
from constructs import Construct
|
||||||
|
|
||||||
|
|
@ -23,7 +22,8 @@ class WorkorderIngestStack(Stack):
|
||||||
|
|
||||||
# --- S3 bucket for raw emails ---
|
# --- S3 bucket for raw emails ---
|
||||||
email_bucket = s3.Bucket(
|
email_bucket = s3.Bucket(
|
||||||
self, "EmailBucket",
|
self,
|
||||||
|
"EmailBucket",
|
||||||
bucket_name=f"workorder-ingest-emails-{self.account}",
|
bucket_name=f"workorder-ingest-emails-{self.account}",
|
||||||
removal_policy=RemovalPolicy.RETAIN,
|
removal_policy=RemovalPolicy.RETAIN,
|
||||||
lifecycle_rules=[
|
lifecycle_rules=[
|
||||||
|
|
@ -33,7 +33,8 @@ class WorkorderIngestStack(Stack):
|
||||||
|
|
||||||
# --- DynamoDB tables ---
|
# --- DynamoDB tables ---
|
||||||
work_orders_table = dynamodb.Table(
|
work_orders_table = dynamodb.Table(
|
||||||
self, "WorkOrdersTable",
|
self,
|
||||||
|
"WorkOrdersTable",
|
||||||
table_name="WorkOrders",
|
table_name="WorkOrders",
|
||||||
partition_key=dynamodb.Attribute(
|
partition_key=dynamodb.Attribute(
|
||||||
name="work_order_id",
|
name="work_order_id",
|
||||||
|
|
@ -68,7 +69,8 @@ class WorkorderIngestStack(Stack):
|
||||||
)
|
)
|
||||||
|
|
||||||
comments_table = dynamodb.Table(
|
comments_table = dynamodb.Table(
|
||||||
self, "CommentsTable",
|
self,
|
||||||
|
"CommentsTable",
|
||||||
table_name="WorkOrderComments",
|
table_name="WorkOrderComments",
|
||||||
partition_key=dynamodb.Attribute(
|
partition_key=dynamodb.Attribute(
|
||||||
name="work_order_id",
|
name="work_order_id",
|
||||||
|
|
@ -84,14 +86,16 @@ class WorkorderIngestStack(Stack):
|
||||||
|
|
||||||
# --- Secrets Manager for Anthropic API key ---
|
# --- Secrets Manager for Anthropic API key ---
|
||||||
anthropic_secret = secretsmanager.Secret(
|
anthropic_secret = secretsmanager.Secret(
|
||||||
self, "AnthropicApiKey",
|
self,
|
||||||
|
"AnthropicApiKey",
|
||||||
secret_name="workorder-ingest/anthropic-api-key",
|
secret_name="workorder-ingest/anthropic-api-key",
|
||||||
description="Anthropic API key for work order email parsing",
|
description="Anthropic API key for work order email parsing",
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Lambda function ---
|
# --- Lambda function ---
|
||||||
email_processor = lambda_.Function(
|
email_processor = lambda_.Function(
|
||||||
self, "EmailProcessor",
|
self,
|
||||||
|
"EmailProcessor",
|
||||||
function_name="workorder-email-processor",
|
function_name="workorder-email-processor",
|
||||||
runtime=lambda_.Runtime.PYTHON_3_12,
|
runtime=lambda_.Runtime.PYTHON_3_12,
|
||||||
handler="handler.handler",
|
handler="handler.handler",
|
||||||
|
|
@ -101,10 +105,11 @@ class WorkorderIngestStack(Stack):
|
||||||
image=lambda_.Runtime.PYTHON_3_12.bundling_image,
|
image=lambda_.Runtime.PYTHON_3_12.bundling_image,
|
||||||
platform="linux/amd64",
|
platform="linux/amd64",
|
||||||
command=[
|
command=[
|
||||||
"bash", "-c",
|
"bash",
|
||||||
|
"-c",
|
||||||
"pip install --platform manylinux2014_x86_64 --only-binary=:all: "
|
"pip install --platform manylinux2014_x86_64 --only-binary=:all: "
|
||||||
"-r requirements.txt -t /asset-output && "
|
"-r requirements.txt -t /asset-output && "
|
||||||
"cp -r . /asset-output/"
|
"cp -r . /asset-output/",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -132,7 +137,9 @@ class WorkorderIngestStack(Stack):
|
||||||
|
|
||||||
# --- SES Receipt Rule ---
|
# --- SES Receipt Rule ---
|
||||||
rule_set = ses.ReceiptRuleSet.from_receipt_rule_set_name(
|
rule_set = ses.ReceiptRuleSet.from_receipt_rule_set_name(
|
||||||
self, "ExistingRuleSet", "INBOUND_MAIL",
|
self,
|
||||||
|
"ExistingRuleSet",
|
||||||
|
"INBOUND_MAIL",
|
||||||
)
|
)
|
||||||
|
|
||||||
rule_set.add_rule(
|
rule_set.add_rule(
|
||||||
|
|
@ -148,7 +155,8 @@ class WorkorderIngestStack(Stack):
|
||||||
|
|
||||||
# --- Web UI Lambda ---
|
# --- Web UI Lambda ---
|
||||||
web_ui = lambda_.Function(
|
web_ui = lambda_.Function(
|
||||||
self, "WebUI",
|
self,
|
||||||
|
"WebUI",
|
||||||
function_name="workorder-web-ui",
|
function_name="workorder-web-ui",
|
||||||
runtime=lambda_.Runtime.PYTHON_3_12,
|
runtime=lambda_.Runtime.PYTHON_3_12,
|
||||||
handler="handler.handler",
|
handler="handler.handler",
|
||||||
|
|
@ -169,4 +177,6 @@ class WorkorderIngestStack(Stack):
|
||||||
auth_type=lambda_.FunctionUrlAuthType.NONE,
|
auth_type=lambda_.FunctionUrlAuthType.NONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
cdk.CfnOutput(self, "WebUIUrl", value=web_url.url, description="Work Order Dashboard URL")
|
cdk.CfnOutput(
|
||||||
|
self, "WebUIUrl", value=web_url.url, description="Work Order Dashboard URL"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email import policy
|
from email import policy
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import anthropic
|
import anthropic
|
||||||
import boto3
|
import boto3
|
||||||
|
|
@ -253,7 +252,9 @@ def handler(event, context):
|
||||||
|
|
||||||
# Extract structured data with Claude
|
# Extract structured data with Claude
|
||||||
parsed = extract_with_claude(email_data)
|
parsed = extract_with_claude(email_data)
|
||||||
logger.info(f"Parsed: type={parsed.get('email_type')}, wo={parsed.get('work_order_id')}")
|
logger.info(
|
||||||
|
f"Parsed: type={parsed.get('email_type')}, wo={parsed.get('work_order_id')}"
|
||||||
|
)
|
||||||
|
|
||||||
if not parsed.get("work_order_id"):
|
if not parsed.get("work_order_id"):
|
||||||
logger.warning(f"No work order ID found in email, skipping: {key}")
|
logger.warning(f"No work order ID found in email, skipping: {key}")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import email
|
import email
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
@ -120,6 +119,8 @@ def handler(event, context):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Saved vendor reply: WO={wo_number} DSP={dispatch_number} from={sender_email}")
|
logger.info(
|
||||||
|
f"Saved vendor reply: WO={wo_number} DSP={dispatch_number} from={sender_email}"
|
||||||
|
)
|
||||||
|
|
||||||
return {"statusCode": 200, "body": "OK"}
|
return {"statusCode": 200, "body": "OK"}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ Serves a simple HTML dashboard for viewing work orders and comments.
|
||||||
Accessed via Lambda Function URL.
|
Accessed via Lambda Function URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
@ -71,7 +69,9 @@ def render_work_order_detail(wo, events):
|
||||||
created = e.get("created_at", "")
|
created = e.get("created_at", "")
|
||||||
text = e.get("text", "")
|
text = e.get("text", "")
|
||||||
badge = render_badge(record_type, RECORD_TYPE_COLORS)
|
badge = render_badge(record_type, RECORD_TYPE_COLORS)
|
||||||
commenter_str = f"<strong>{commenter}</strong> — " if commenter else ""
|
commenter_str = (
|
||||||
|
f"<strong>{commenter}</strong> — " if commenter else ""
|
||||||
|
)
|
||||||
|
|
||||||
border_colors = {
|
border_colors = {
|
||||||
"new_work_order": "#3b82f6",
|
"new_work_order": "#3b82f6",
|
||||||
|
|
@ -95,7 +95,10 @@ def render_work_order_detail(wo, events):
|
||||||
fields = [
|
fields = [
|
||||||
("Description", wo.get("description")),
|
("Description", wo.get("description")),
|
||||||
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
|
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
|
||||||
("Record Type", render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS)),
|
(
|
||||||
|
"Record Type",
|
||||||
|
render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS),
|
||||||
|
),
|
||||||
("Site Code", wo.get("site_code")),
|
("Site Code", wo.get("site_code")),
|
||||||
("Building", wo.get("building")),
|
("Building", wo.get("building")),
|
||||||
("Address", wo.get("address")),
|
("Address", wo.get("address")),
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class WorkOrder:
|
||||||
@dataclass
|
@dataclass
|
||||||
class ParsedEmail:
|
class ParsedEmail:
|
||||||
"""Result of AI parsing an inbound email."""
|
"""Result of AI parsing an inbound email."""
|
||||||
|
|
||||||
email_type: str # EmailType value
|
email_type: str # EmailType value
|
||||||
work_order_id: Optional[str] = None
|
work_order_id: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue