From 1b06450c8aff084163fcb752eeded743ff4188f1 Mon Sep 17 00:00:00 2001 From: aiworkhackshop-design Date: Fri, 10 Apr 2026 17:38:47 -0700 Subject: [PATCH 1/5] Add backend Dockerfile for FastAPI service --- backend/Dockerfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 backend/Dockerfile diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b44983e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim AS base + +# Set working directory for the FastAPI application +WORKDIR /app + +# Copy only the requirements first to leverage Docker layer caching +COPY requirements.txt ./ + +# Upgrade pip and install dependencies +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# Copy the application code +COPY app ./app + +# Expose the port FastAPI will run on +EXPOSE 8000 + +# Default command runs the FastAPI app with uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] From daa0cafb49c7fc349dd8dab0d9901aa38b761f47 Mon Sep 17 00:00:00 2001 From: aiworkhackshop-design Date: Fri, 10 Apr 2026 17:44:03 -0700 Subject: [PATCH 2/5] Add main.py with summarization and Frappe transfer endpoints Implement summarization API with fallback to naive summarizer when no OpenAI key is set, and add /transfer/frappe endpoint using httpx and environment variables for secure Frappe API forwarding. --- backend/app/main.py | 105 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 backend/app/main.py diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..e5c5146 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,105 @@ +# Entry point for the FastAPI backend + +"""Main entry point for the FastAPI backend. + +This module exposes a minimal API surface suitable for building a micro SaaS +application. In addition to a simple root endpoint, it provides a text +summarization service built on top of LangChain and OpenAI. If no +`OPENAI_API_KEY` environment variable is present, the service will fall back + to a naive summarization algorithm that extracts the first couple of + sentences of the input. This makes the API usable out of the box without + external API credentials. + +Two placeholder endpoints (`/auth/login` and `/billing/checkout`) are also +provided to serve as stubs for future authentication and billing logic +(e.g. integration with Stripe). These currently return static responses and +can be expanded later as the product matures. +""" +import os +from typing import List + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from starlette import status +import httpx + +app = FastAPI(title="AI Micro SaaS API") + +# root endpoint +@app.get("/") +def read_root(): + return {"message": "Welcome to the AI Micro SaaS backend"} + + +class TextInput(BaseModel): + text: str + + +@app.post("/summarize") +async def summarize_text(data: TextInput): + text = data.text.strip() if data.text else "" + if not text: + raise HTTPException(status_code=400, detail="Text is required for summarization") + + openai_key = os.getenv("OPENAI_API_KEY") + try: + if openai_key: + from langchain_openai import ChatOpenAI + from langchain.chains.summarize import load_summarize_chain + from langchain.docstore.document import Document + llm = ChatOpenAI(openai_api_key=openai_key, temperature=0.0) + docs: List[Document] = [Document(page_content=text)] + chain = load_summarize_chain(llm, chain_type="stuff") + summary: str = chain.run(docs) + else: + sentences = [s.strip() for s in text.replace("\n", " ").split(".") if s.strip()] + if len(sentences) >= 2: + summary = ". ".join(sentences[:2]) + "." + else: + summary = (text[:200] + "…") if len(text) > 200 else text + except Exception: + sentences = [s.strip() for s in text.replace("\n", " ").split(".") if s.strip()] + if len(sentences) >= 2: + summary = ". ".join(sentences[:2]) + "." + else: + summary = (text[:200] + "…") if len(text) > 200 else text + return {"summary": summary} + + +@app.post("/auth/login") +async def login(username: str = "user", password: str = "password"): + return {"status": "success", "message": f"Authenticated as {username}"} + + +@app.post("/billing/checkout") +async def create_checkout_session(plan: str = "basic"): + dummy_url = f"https://example.com/checkout?plan={plan}" + return {"checkout_url": dummy_url} + + +# Frappe data transfer +class FrappeTransferInput(BaseModel): + doctype: str + payload: dict + + +@app.post("/transfer/frappe", status_code=status.HTTP_200_OK) +async def transfer_to_frappe(data: FrappeTransferInput): + frappe_base_url = os.getenv("FRAPPE_API_BASE_URL") + api_key = os.getenv("FRAPPE_API_KEY") + api_secret = os.getenv("FRAPPE_API_SECRET") + if not frappe_base_url or not api_key or not api_secret: + raise HTTPException(status_code=500, detail="Frappe configuration variables are missing") + target_url = frappe_base_url.rstrip("/") + f"/resource/{data.doctype}" + headers = { + "Authorization": f"token {api_key}:{api_secret}", + "Content-Type": "application/json", + } + try: + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post(target_url, json=data.payload, headers=headers) + if response.status_code >= 400: + raise HTTPException(status_code=response.status_code, detail=response.text) + return response.json() + except httpx.RequestError as e: + raise HTTPException(status_code=502, detail=f"Error communicating with Frappe: {str(e)}") From 65fe61211987cdcc573efbc5addca9c1788d9d4c Mon Sep 17 00:00:00 2001 From: aiworkhackshop-design Date: Fri, 10 Apr 2026 17:46:02 -0700 Subject: [PATCH 3/5] Add backend requirements.txt with dependencies Include fastapi, uvicorn[standard], pydantic, langchain, langchain-openai, httpx for summarization and Frappe endpoints. --- backend/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/requirements.txt diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..291933a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +pydantic +langchain +langchain-openai +httpx From 283074452aa55533a01a8a1f9aa56d9421a79c3e Mon Sep 17 00:00:00 2001 From: aiworkhackshop-design Date: Fri, 10 Apr 2026 17:49:43 -0700 Subject: [PATCH 4/5] Add frontend Dockerfile for Next.js service Provide Dockerfile to build and run the Next.js app in development mode. Copies dependencies and sets default command to npm run dev. --- frontend/Dockerfile | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/Dockerfile diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..21eb0c8 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS base + +# Set working directory for the Next.js app +WORKDIR /app + +# Copy package definition and install dependencies first +COPY package.json ./ +COPY package-lock.json* ./ + +# Install dependencies with npm. The || true handles cases where +# package-lock.json is absent so npm falls back to installing from package.json. +RUN npm install --legacy-peer-deps || true + +# Copy the rest of the application code +COPY . . + +# Expose the Next.js development port +EXPOSE 3000 + +# Default command: run the Next.js development server. For production +# a `npm run build && npm run start` could be used instead. +CMD ["npm", "run", "dev"] From 50b015cb06e531bfdea5afcd072b7ebfc86d4117 Mon Sep 17 00:00:00 2001 From: aiworkhackshop-design Date: Fri, 10 Apr 2026 17:54:40 -0700 Subject: [PATCH 5/5] Add test_frappe_transfer.py script Provide an asynchronous test script to verify summarization and Frappe transfer endpoints using environment variables. --- test_frappe_transfer.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test_frappe_transfer.py diff --git a/test_frappe_transfer.py b/test_frappe_transfer.py new file mode 100644 index 0000000..ca080c7 --- /dev/null +++ b/test_frappe_transfer.py @@ -0,0 +1,27 @@ +import os +import json +import asyncio +import httpx + +async def run(): + base_url = os.getenv("FASTAPI_URL", "http://localhost:8000") + async with httpx.AsyncClient() as client: + # Test summarization endpoint + text = "This is a test. This second sentence ensures summarization picks two sentences." + response = await client.post(f"{base_url}/summarize", json={"text": text}) + print("Summarize response:", response.json()) + + # Prepare data for Frappe transfer + doctype = os.getenv("FRAPPE_DOCTYPE", "ToDo") + payload_env = os.getenv("FRAPPE_PAYLOAD", '{"description": "Sample via API"}') + try: + payload = json.loads(payload_env) + except json.JSONDecodeError: + payload = {"data": payload_env} + response2 = await client.post( + f"{base_url}/transfer/frappe", json={"doctype": doctype, "payload": payload} + ) + print("Frappe response:", response2.json()) + +if __name__ == "__main__": + asyncio.run(run())