Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf7c88d
fix(coaching): stabilize staging deploy - refs #187
mottych Dec 23, 2025
094b9b3
fix(coaching): align langchain pins for CI - refs #187
mottych Dec 23, 2025
074f39a
chore(repo): add docs sync workflow and S3 ownership hardening - refs…
mottych Dec 25, 2025
93ba18a
Merge feature/docs-sync-workflow into dev - refs #190
mottych Dec 25, 2025
52e4b7d
feat(admin): add template test endpoint debug run - refs #191
mottych Dec 25, 2025
0119dee
Merge feature/admin-template-test-endpoint into dev - refs #191
mottych Dec 25, 2025
b83cfb8
feat(coaching): align website_scan schema and prompts - refs #192
mottych Dec 25, 2025
fc5589b
docs: update website_scan schema example - refs #192
mottych Dec 25, 2025
3464082
chore(docs): seed shared docs folder for sync
mottych Dec 25, 2025
70ddb6d
chore(docs): update shared readme for sync
mottych Dec 25, 2025
a580b95
feat(kpis-api): Add comprehensive KPI API specification with 7 endpoi…
mottych Dec 25, 2025
763d388
docs: sync shared documentation from PurposePath_Web [docs-sync]
actions-user Dec 25, 2025
1fcc5d3
docs: sync shared documentation from PurposePath_Api [docs-sync]
actions-user Dec 25, 2025
a6c9768
docs: sync shared documentation from PurposePath_Api [docs-sync]
actions-user Dec 25, 2025
be452c5
docs: sync shared documentation from PurposePath_Api [docs-sync]
actions-user Dec 25, 2025
b3eead1
feat(coaching): update retrieval methods for .NET API refactor - refs…
mottych Dec 25, 2025
1214755
Merge feature/update-api-retrieval-methods-193 into dev - refs #193
mottych Dec 25, 2025
84a96b3
fix(tests): update business_api_client tests for new API params - ref…
mottych Dec 25, 2025
1878cdf
fix(tests): update WebsiteScanResponse tests for new model structure
mottych Dec 25, 2025
f03b521
fix(infra): add stage filtering to EventBridge events to prevent cros…
mottych Dec 25, 2025
5232ee4
fix: format test_onboarding_models.py
mottych Dec 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
omit =
coaching/src/api/*
coaching/src/scripts/*
coaching/src/infrastructure/*
coaching/src/llm/*
167 changes: 167 additions & 0 deletions .github/workflows/sync-shared-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Cross-Repository Documentation Sync Workflow
#
# This workflow syncs the docs/shared/ folder across all PurposePath repositories.
# When documentation is updated in any repo, it propagates to all other repos.
#
# The docs/shared/ folder contains:
# - Specifications (backend/frontend API specs)
# - Shared documentation across all repos
#
# Setup Requirements:
# 1. Create a PAT with 'repo' scope that has access to all PurposePath repos
# 2. Add the PAT as a secret named 'DOCS_SYNC_PAT' to all repositories
# 3. Copy this workflow file to all repositories
#
# Synced Repositories:
# - PurposePath_Api (Backend API)
# - PurposePath_Web (User Frontend)
# - PurposePath_AI (Coaching Service)
# - PurposePath_Admin (Admin Frontend)
# - PurposePath_Kpi (KPI Integration)

name: Sync Shared Documentation

on:
push:
branches:
- dev
- main
paths:
- 'docs/shared/**'

# Allow manual trigger for initial sync or troubleshooting
workflow_dispatch:
inputs:
target_branch:
description: 'Branch to sync to (dev or main)'
required: true
default: 'dev'
type: choice
options:
- dev
- main

# Prevent concurrent syncs to avoid race conditions
concurrency:
group: docs-sync-${{ github.ref }}
cancel-in-progress: false

jobs:
sync-docs:
# Skip if this push was from a sync operation (prevents infinite loops)
if: "!contains(github.event.head_commit.message, '[docs-sync]')"

runs-on: ubuntu-latest

env:
# List of repositories to sync to (excluding the source repo)
ALL_REPOS: |
PurposePath_Api
PurposePath_Web
PurposePath_AI
PurposePath_Admin
PurposePath_Kpi

steps:
- name: Checkout source repository
uses: actions/checkout@v4
with:
fetch-depth: 0
path: source

- name: Get current repo name
id: current-repo
run: echo "name=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT

- name: Get commit info
id: commit-info
run: |
cd source
echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "message=$(git log -1 --pretty=%s | head -c 50)" >> $GITHUB_OUTPUT
echo "author=$(git log -1 --pretty=%an)" >> $GITHUB_OUTPUT

- name: Sync to other repositories
env:
GH_TOKEN: ${{ secrets.DOCS_SYNC_PAT }}
CURRENT_REPO: ${{ steps.current-repo.outputs.name }}
SOURCE_SHA: ${{ steps.commit-info.outputs.sha }}
SOURCE_MESSAGE: ${{ steps.commit-info.outputs.message }}
SOURCE_AUTHOR: ${{ steps.commit-info.outputs.author }}
SOURCE_BRANCH: ${{ github.event.inputs.target_branch || github.ref_name }}
run: |
# Configure git
git config --global user.name "GitHub Actions (Docs Sync)"
git config --global user.email "actions@github.com"

# Process each repository
echo "$ALL_REPOS" | while read -r repo; do
# Skip empty lines and current repo
[ -z "$repo" ] && continue
[ "$repo" = "$CURRENT_REPO" ] && continue

echo "=========================================="
echo "Syncing to: $repo"
echo "=========================================="

# Clone target repo
TARGET_DIR="target-$repo"
if ! git clone --depth=1 --branch="$SOURCE_BRANCH" \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository_owner }}/${repo}.git" \
"$TARGET_DIR" 2>/dev/null; then
echo "WARNING: Branch $SOURCE_BRANCH does not exist in $repo, skipping"
continue
fi

# Create docs/shared if it doesn't exist
mkdir -p "$TARGET_DIR/docs/shared"

# Sync docs/shared/ if it exists in source
if [ -d "source/docs/shared" ]; then
rsync -av --delete \
--exclude='.git' \
"source/docs/shared/" "$TARGET_DIR/docs/shared/"
fi

# Check for changes
cd "$TARGET_DIR"
if git diff --quiet && git diff --cached --quiet; then
echo "No changes needed for $repo"
cd ..
continue
fi

# Stage and commit changes
git add docs/shared/

git commit -m "docs: sync shared documentation from $CURRENT_REPO [docs-sync]" \
-m "Synced from: $CURRENT_REPO@$SOURCE_SHA" \
-m "Original commit: $SOURCE_MESSAGE" \
-m "Original author: $SOURCE_AUTHOR" \
-m "Branch: $SOURCE_BRANCH" \
-m "[skip ci]"

# Push changes
if git push; then
echo "Successfully synced to $repo"
else
echo "Failed to push to $repo"
fi

cd ..
done

echo "=========================================="
echo "Sync complete!"
echo "=========================================="

- name: Summary
run: |
echo "## Documentation Sync Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Source Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch:** ${{ github.event.inputs.target_branch || github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** ${{ steps.commit-info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- **Message:** $(echo '${{ steps.commit-info.outputs.message }}' | head -c 50)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Documentation in docs/shared/ has been synced to all PurposePath repositories." >> $GITHUB_STEP_SUMMARY
3 changes: 3 additions & 0 deletions coaching/pulumi/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@
{
"source": ["purposepath.ai"],
"detail-type": ["ai.job.created"],
"detail": {
"stage": [stack] # Filter by environment to prevent cross-stage execution
},
}
),
tags={"Environment": stack, "Service": "coaching-ai"},
Expand Down
36 changes: 4 additions & 32 deletions coaching/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,38 +1,10 @@
# Core dependencies
fastapi==0.109.0
mangum==0.17.0
uvicorn[standard]==0.27.0
pydantic==2.10.4
pydantic-settings==2.7.1
python-dotenv==1.0.0
email-validator==2.1.0.post1

# AWS SDK
boto3==1.35.74
botocore==1.35.74

# LLM and AI - pinned to compatible versions
langchain==0.3.13
langchain-aws==0.2.9
langchain-anthropic==0.3.9
langchain-openai==0.2.14
langgraph==0.2.62
tiktoken==0.8.0
-r requirements.txt

# Database
redis==5.0.0
boto3-stubs[dynamodb,s3]==1.35.74

# Utilities
python-jose[cryptography]==3.3.0
# Additional runtime extras
email-validator==2.1.0.post1
python-multipart==0.0.20
httpx==0.27.2
structlog==23.2.0
pyyaml==6.0.1
requests==2.31.0
html2text==2024.2.26

# Development
# Development tools
pytest==7.4.0
pytest-asyncio==0.21.0
pytest-mock==3.11.1
Expand Down
1 change: 1 addition & 0 deletions coaching/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ openai==1.109.1
numpy<2.0 # Pin to avoid compilation in Lambda
google-cloud-aiplatform==1.74.0
anthropic==0.72.0
langchain==0.3.12
langchain-core==0.3.63
langchain-anthropic==0.3.9
langchain-aws==0.2.9
Expand Down
7 changes: 6 additions & 1 deletion coaching/src/api/dependencies/async_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ async def get_event_publisher() -> EventBridgePublisher:
region_name=settings.aws_region,
event_bus_name="default", # Using default EventBridge bus
source="purposepath.ai",
stage=settings.stage,
)
logger.info(
"EventBridgePublisher initialized",
source="purposepath.ai",
stage=settings.stage,
)
logger.info("EventBridgePublisher initialized", source="purposepath.ai")

return _event_publisher

Expand Down
90 changes: 71 additions & 19 deletions coaching/src/api/models/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,78 @@ class ProductInfo(BaseModel):
)


class WebsiteScanResponse(BaseModel):
"""Response from website scan with extracted business information.
class WebsiteScanCompanyProfile(BaseModel):
"""Company profile details extracted from the website."""

The LLM analyzes the website content and extracts structured business
information to pre-fill the onboarding form.
"""
company_name: str = Field(..., description="Public-facing company name")
legal_name: str = Field(..., description="Registered legal entity name")
tagline: str = Field(..., description="Marketing tagline or headline")
overview: str = Field(..., description="One-paragraph business overview")

products: list[ProductInfo] = Field(
...,
description="List of products/services offered by the business",
)
niche: str = Field(
...,
description="Target market and business niche description (2-3 sentences)",
)
ica: str = Field(
...,
description="Ideal Customer Avatar - demographics, pain points, and goals",

class WebsiteScanTargetMarket(BaseModel):
"""Target market insights."""

primary_audience: str = Field(..., description="Primary audience or buyer persona")
segments: list[str] = Field(..., description="Market segments served")
pain_points: list[str] = Field(..., description="Key pain points addressed")


class WebsiteScanOffers(BaseModel):
"""Products and offers highlighted on the site."""

primary_product: str = Field(..., description="Main product or offer")
categories: list[str] = Field(..., description="Product/solution categories")
features: list[str] = Field(..., description="Notable features or capabilities")
differentiators: list[str] = Field(..., description="Differentiators vs competitors")


class WebsiteScanTestimonial(BaseModel):
"""Customer testimonial snippet."""

quote: str = Field(..., description="Customer quote")
attribution: str = Field(..., description="Attribution for the quote")


class WebsiteScanCredibility(BaseModel):
"""Signals that build trust."""

notable_clients: list[str] = Field(..., description="List of notable clients")
testimonials: list[WebsiteScanTestimonial] = Field(
default_factory=list, description="Testimonials pulled from the site"
)
value_proposition: str = Field(
...,
description="Main value proposition - what makes this business unique (1-2 sentences)",


class WebsiteScanSupportingAsset(BaseModel):
"""Supporting asset promoted on the page."""

label: str = Field(..., description="Display label for the asset")
url: str = Field(..., description="URL to the asset")


class WebsiteScanConversion(BaseModel):
"""Conversion-oriented content from the site."""

primary_cta_text: str = Field(..., description="Primary call-to-action text")
primary_cta_url: str = Field(..., description="Primary call-to-action URL")
supporting_assets: list[WebsiteScanSupportingAsset] = Field(
default_factory=list, description="Supporting assets for conversion"
)


class WebsiteScanResponse(BaseModel):
"""Data payload for website_scan topic results (no wrappers)."""

scan_id: str = Field(..., description="Unique identifier for this scan run")
captured_at: str = Field(..., description="ISO8601 timestamp when the scan was captured")
source_url: str = Field(..., description="URL that was scanned")
company_profile: WebsiteScanCompanyProfile
target_market: WebsiteScanTargetMarket
offers: WebsiteScanOffers
credibility: WebsiteScanCredibility
conversion: WebsiteScanConversion


# Coaching endpoint models
class OnboardingCoachingRequest(BaseModel):
"""Request for onboarding coaching assistance."""
Expand Down Expand Up @@ -170,6 +217,11 @@ class OnboardingReviewResponse(BaseModel):
"OnboardingSuggestionRequest",
"OnboardingSuggestionResponse",
"SuggestionVariation",
"WebsiteScanCompanyProfile",
"WebsiteScanConversion",
"WebsiteScanCredibility",
"WebsiteScanOffers",
"WebsiteScanRequest",
"WebsiteScanResponse",
"WebsiteScanTargetMarket",
]
Loading
Loading