1818import os
1919import uuid
2020import logging
21+ import time
22+ from collections import defaultdict
2123from typing import Dict , Optional
2224from contextlib import asynccontextmanager
2325
24- from fastapi import FastAPI , HTTPException
26+ from fastapi import FastAPI , HTTPException , Request
2527from fastapi .middleware .cors import CORSMiddleware
26- from fastapi .responses import StreamingResponse
28+ from fastapi .responses import StreamingResponse , JSONResponse
2729from pydantic import BaseModel
2830
2931from deploy .orchestrator import GoogleAdsAgent , create_agent_system
3335# ── Session Store ─────────────────────────────────────────────────────────────
3436sessions : Dict [str , GoogleAdsAgent ] = {}
3537
38+ # ── Rate Limiting ─────────────────────────────────────────────────────────────
39+ rate_limit_store : Dict [str , list ] = defaultdict (list )
40+ RATE_LIMIT_WINDOW = 60
41+ RATE_LIMIT_MAX = int (os .environ .get ("RATE_LIMIT_MAX" , "30" ))
42+
3643
3744@asynccontextmanager
3845async def lifespan (app : FastAPI ):
3946 """Startup/shutdown lifecycle."""
4047 logger .info ("Google Ads Agent server starting..." )
41- # Validate credentials on startup
4248 required_keys = ["ANTHROPIC_API_KEY" ]
4349 missing = [k for k in required_keys if not os .environ .get (k )]
4450 if missing :
@@ -50,21 +56,43 @@ async def lifespan(app: FastAPI):
5056 sessions .clear ()
5157
5258
59+ ALLOWED_ORIGINS = os .environ .get (
60+ "ALLOWED_ORIGINS" ,
61+ "http://localhost:3000,http://localhost:8000,http://127.0.0.1:3000"
62+ ).split ("," )
63+
5364app = FastAPI (
5465 title = "Google Ads Agent API" ,
5566 description = "Enterprise Google Ads management powered by Claude" ,
56- version = "10 .0.0" ,
67+ version = "2 .0.0" ,
5768 lifespan = lifespan ,
5869)
5970
6071app .add_middleware (
6172 CORSMiddleware ,
62- allow_origins = [ "*" ] ,
63- allow_methods = ["* " ],
64- allow_headers = ["* " ],
73+ allow_origins = ALLOWED_ORIGINS ,
74+ allow_methods = ["POST" , "GET" , "DELETE" , "OPTIONS " ],
75+ allow_headers = ["Content-Type" , "Authorization " ],
6576)
6677
6778
79+ @app .middleware ("http" )
80+ async def rate_limit_middleware (request : Request , call_next ):
81+ """Simple in-memory rate limiter per IP."""
82+ client_ip = request .client .host if request .client else "unknown"
83+ now = time .time ()
84+ rate_limit_store [client_ip ] = [
85+ t for t in rate_limit_store [client_ip ] if t > now - RATE_LIMIT_WINDOW
86+ ]
87+ if len (rate_limit_store [client_ip ]) >= RATE_LIMIT_MAX :
88+ return JSONResponse (
89+ status_code = 429 ,
90+ content = {"error" : "Rate limit exceeded. Try again shortly." },
91+ )
92+ rate_limit_store [client_ip ].append (now )
93+ return await call_next (request )
94+
95+
6896# ── Models ────────────────────────────────────────────────────────────────────
6997
7098class ChatRequest (BaseModel ):
@@ -121,8 +149,8 @@ async def chat(request: ChatRequest):
121149 tool_calls_made = tool_calls ,
122150 )
123151 except Exception as e :
124- logger .error (f"Chat error: { e } " )
125- raise HTTPException (status_code = 500 , detail = str ( e ) )
152+ logger .error (f"Chat error: { e } " , exc_info = True )
153+ raise HTTPException (status_code = 500 , detail = "An internal error occurred. Check server logs." )
126154
127155
128156@app .post ("/sessions" , response_model = SessionInfo )
0 commit comments