Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
105 changes: 105 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -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)}")
6 changes: 6 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
pydantic
langchain
langchain-openai
httpx
22 changes: 22 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
27 changes: 27 additions & 0 deletions test_frappe_transfer.py
Original file line number Diff line number Diff line change
@@ -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())