diff --git a/competitor-analysis-app/competitor_analysis_orchestrator.py b/competitor-analysis-app/competitor_analysis_orchestrator.py index 8ab68eb7..01f6ed5c 100644 --- a/competitor-analysis-app/competitor_analysis_orchestrator.py +++ b/competitor-analysis-app/competitor_analysis_orchestrator.py @@ -1,5 +1,6 @@ import os import json +import asyncio from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -7,14 +8,14 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from typing import List, Dict, Any, Optional -import google.generativeai as genai -from google.generativeai.types import HarmCategory, HarmBlockThreshold +# Modern SDK only +from google import genai +from google.genai import types # Load environment variables load_dotenv() API_KEY = os.getenv("GEMINI_API_KEY") -# Fallback: check for API Key in a file (common in this project's Docker setup) if not API_KEY: key_file_path = os.getenv("GEMINI_API_KEY_FILE", "/app/gemini_api_key.txt") if os.path.exists(key_file_path): @@ -24,23 +25,11 @@ if not API_KEY: if not API_KEY: raise ValueError("GEMINI_API_KEY environment variable or file not set") -genai.configure(api_key=API_KEY) -# Use a candidate list for models as per migration guide -MODEL_CANDIDATES = ['gemini-1.5-pro', 'gemini-1.0-pro'] # Added 1.0-pro as fallback -model = None -for candidate in MODEL_CANDIDATES: - try: - model = genai.GenerativeModel(candidate) - print(f"DEBUG: Using Gemini model: {candidate}") - break - except Exception as e: - print(f"DEBUG: Could not load model {candidate}: {e}") - if "404" in str(e): - continue - raise e -if not model: - raise ValueError(f"No suitable Gemini model found from candidates: {MODEL_CANDIDATES}") +# Initialize the modern client +client = genai.Client(api_key=API_KEY) +MODEL_CANDIDATES = ['gemini-1.5-flash', 'gemini-1.5-pro'] # Directly set to a modern, fast model +print(f"DEBUG: Initialized with MODEL_NAME={MODEL_NAME}") app = FastAPI() @@ -53,22 +42,31 @@ app.add_middleware( allow_headers=["*"], ) -def parse_json_response(text: str) -> Any: - """Parses JSON response, stripping markdown code blocks.""" +def parse_json_response(response) -> Any: + """Parses JSON response from the modern SDK robustly.""" try: - # Clean the text, removing markdown code block fences - cleaned_text = text.replace('```json', '').replace('```', '').strip() - # Handle cases where the model might return a list instead of a direct object + text = response.text + if not text: + return {} # Return empty dict on empty response + + cleaned_text = text.strip() + if cleaned_text.startswith("```"): + lines = cleaned_text.splitlines() + if lines[0].startswith("```"): + lines = lines[1:] + if lines[-1].startswith("```"): + lines = lines[:-1] + cleaned_text = "\n".join(lines).strip() + result = json.loads(cleaned_text) if isinstance(result, list) and result: - # If it's a list, assume the first element is the intended object return result[0] return result - except json.JSONDecodeError as e: - print(f"Failed to parse JSON: {e}\nOriginal text: {text}") - raise ValueError("Invalid JSON response from API") + except Exception as e: + print(f"CRITICAL: Failed to parse JSON: {e}\nRaw text: {getattr(response, 'text', 'NO TEXT')}") + return {} # Return empty dict to avoid frontend crash -# --- Schemas (ported from TypeScript) --- +# --- Schemas (Native Python Dictionaries) --- evidence_schema = { "type": "object", "properties": { @@ -81,8 +79,8 @@ evidence_schema = { product_schema = { "type": "object", "properties": { - "name": {"type": "string", "description": "Product name"}, - "purpose": {"type": "string", "description": "Purpose description (1-2 sentences)"}, + "name": {"type": "string"}, + "purpose": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}, }, "required": ['name', 'purpose', 'evidence'] @@ -91,162 +89,85 @@ product_schema = { industry_schema = { "type": "object", "properties": { - "name": {"type": "string", "description": "Name of the target industry"}, + "name": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}, }, "required": ['name', 'evidence'] } - -# --- Request Models for FastAPI --- +# --- Request Models --- class ProductDetailsRequest(BaseModel): - name: str - url: str - language: str - + name: str; url: str; language: str class FetchStep1DataRequest(BaseModel): - start_url: str - language: str - + start_url: str; language: str class ProductModel(BaseModel): - name: str - purpose: str - evidence: List[Dict[str, str]] - + name: str; purpose: str; evidence: List[Dict[str, str]] class TargetIndustryModel(BaseModel): - name: str - evidence: List[Dict[str, str]] - + name: str; evidence: List[Dict[str, str]] class FetchStep2DataRequest(BaseModel): - products: List[ProductModel] - industries: List[TargetIndustryModel] - language: str - + products: List[ProductModel]; industries: List[TargetIndustryModel]; language: str class KeywordModel(BaseModel): - term: str - rationale: str - + term: str; rationale: str class FetchStep3DataRequest(BaseModel): - keywords: List[KeywordModel] - market_scope: str - language: str - + keywords: List[KeywordModel]; market_scope: str; language: str class CompanyModel(BaseModel): - name: str - start_url: str - + name: str; start_url: str class CompetitorCandidateModel(BaseModel): - name: str - url: str - confidence: float - why: str - evidence: List[Dict[str, str]] - + name: str; url: str; confidence: float; why: str; evidence: List[Dict[str, str]] class FetchStep4DataRequest(BaseModel): - company: CompanyModel - competitors: List[CompetitorCandidateModel] - language: str - + company: CompanyModel; competitors: List[CompetitorCandidateModel]; language: str class AnalysisModel(BaseModel): - competitor: Dict[str, str] - portfolio: List[Dict[str, str]] - target_industries: List[str] - delivery_model: str - overlap_score: int - differentiators: List[str] - evidence: List[Dict[str, str]] - + competitor: Dict[str, str]; portfolio: List[Dict[str, str]]; target_industries: List[str] + delivery_model: str; overlap_score: int; differentiators: List[str]; evidence: List[Dict[str, str]] class FetchStep5DataSilverBulletsRequest(BaseModel): - company: CompanyModel - analyses: List[AnalysisModel] - language: str - + company: CompanyModel; analyses: List[AnalysisModel]; language: str class SilverBulletModel(BaseModel): - competitor_name: str - statement: str - + competitor_name: str; statement: str class FetchStep6DataConclusionRequest(BaseModel): - company: CompanyModel - products: List[ProductModel] - industries: List[TargetIndustryModel] - analyses: List[AnalysisModel] - silver_bullets: List[SilverBulletModel] - language: str - + company: CompanyModel; products: List[ProductModel]; industries: List[TargetIndustryModel] + analyses: List[AnalysisModel]; silver_bullets: List[SilverBulletModel]; language: str class FetchStep7DataBattlecardsRequest(BaseModel): - company: CompanyModel - analyses: List[AnalysisModel] - silver_bullets: List[SilverBulletModel] - language: str - + company: CompanyModel; analyses: List[AnalysisModel]; silver_bullets: List[SilverBulletModel]; language: str class ShortlistedCompetitorModel(BaseModel): - name: str - url: str - + name: str; url: str class FetchStep8DataReferenceAnalysisRequest(BaseModel): - competitors: List[ShortlistedCompetitorModel] - language: str + competitors: List[ShortlistedCompetitorModel]; language: str +# --- API Helper --- +async def call_gemini_json(prompt: str, schema: dict): + """Calls Gemini with schema enforcement.""" + last_err = None + for model_name in MODEL_CANDIDATES: + try: + config_args = {"response_mime_type": "application/json"} + if schema: + config_args["response_schema"] = schema + + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig(**config_args) + ) + return parse_json_response(response) + except Exception as e: + last_err = e + print(f"DEBUG: Model {model_name} failed: {e}") + if "404" in str(e) or "not supported" in str(e).lower(): + continue + break + raise HTTPException(status_code=500, detail=f"Gemini API Error: {str(last_err)}") # --- Endpoints --- + @app.post("/api/fetchProductDetails") async def fetch_product_details(request: ProductDetailsRequest): - prompts = { - "de": f""" - Analysiere die Webseite {request.url} und beschreibe den Zweck des Produkts "{request.name}" in 1-2 Sätzen. Gib auch die genaue URL und ein relevantes Snippet als Beleg an. - Das "name" Feld im JSON soll der offizielle Produktname sein, wie er auf der Seite gefunden wird, oder "{request.name}" falls nicht eindeutig. - Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Analyze the website {request.url} and describe the purpose of the product "{request.name}" in 1-2 sentences. Also provide the exact URL and a relevant snippet as evidence. - The "name" field in the JSON should be the official product name as found on the page, or "{request.name}" if not clearly stated. - Respond exclusively in JSON format. - """ - } - - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=product_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_product_details: {e}") - raise HTTPException(status_code=500, detail=str(e)) + prompt = r"""Analysiere {url} und beschreibe den Zweck von "{name}" in 1-2 Sätzen. Antworte ausschließlich im JSON-Format.""" + return await call_gemini_json(prompt.format(url=request.url, name=request.name), product_schema) @app.post("/api/fetchStep1Data") async def fetch_step1_data(request: FetchStep1DataRequest): - prompts = { - "de": f""" - Rolle: Research-Agent für B2B-Software-Wettbewerbsanalyse. - Aufgabe: Analysiere die Website {request.start_url} und identifiziere die Hauptprodukte/Lösungen und deren Zielbranchen. - Regeln: - 1. Konzentriere dich auf offizielle Produkt- und Lösungsseiten. - 2. Jede Information (Produkt, Branche) muss mit einer URL und einem kurzen Snippet belegt werden. - 3. Sei präzise und vermeide Spekulationen. - Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Role: Research Agent for B2B Software Competitor Analysis. - Task: Analyze the website {request.start_url} and identify the main products/solutions and their target industries. - Rules: - 1. Focus on official product and solution pages. - 2. Every piece of information (product, industry) must be backed by a URL and a short snippet as evidence. - 3. Be precise and avoid speculation. - Respond exclusively in JSON format. - """ - } - - response_schema = { + prompt = r"""Analysiere die Webseite {url} und identifiziere die Hauptprodukte/Lösungen und deren Zielbranchen. Antworte ausschließlich im JSON-Format.""" + schema = { "type": "object", "properties": { "products": {"type": "array", "items": product_schema}, @@ -254,664 +175,78 @@ async def fetch_step1_data(request: FetchStep1DataRequest): }, "required": ['products', 'target_industries'] } - - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step1_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) + data = await call_gemini_json(prompt.format(url=request.start_url), schema) + # Double check keys for frontend compatibility + if 'products' not in data: data['products'] = [] + if 'target_industries' not in data: data['target_industries'] = [] + return data @app.post("/api/fetchStep2Data") async def fetch_step2_data(request: FetchStep2DataRequest): - prompts = { - "de": f""" - Rolle: Research-Agent. - Aufgabe: Leite aus den folgenden Produkt- und Brancheninformationen 10-25 präzise deutsche und englische Keywords/Suchphrasen für die Wettbewerbsrecherche ab. - Kontext: - Produkte: {', '.join([f'{p.name} ({p.purpose})' for p in request.products])} - Branchen: {', '.join([i.name for i in request.industries])} - Regeln: - 1. Erstelle Cluster: Produktkategorie, Funktionskern, Zielbranchen, Synonyme/Englischvarianten. - 2. Gib für jedes Keyword eine kurze Begründung ("rationale"). - 3. Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Role: Research Agent. - Task: From the following product and industry information, derive 10-25 precise English keywords/search phrases for competitor research. - Context: - Products: {', '.join([f'{p.name} ({p.purpose})' for p in request.products])} - Industries: {', '.join([i.name for i in request.industries])} - Rules: - 1. Create clusters: Product category, core function, target industries, synonyms. - 2. Provide a brief rationale for each keyword. - 3. Respond exclusively in JSON format. - """ - } - - response_schema = { - "type": "object", - "properties": { - "keywords": { - "type": "array", - "items": { - "type": "object", - "properties": { - "term": {"type": "string"}, - "rationale": {"type": "string"} - }, - "required": ['term', 'rationale'] - } - } - }, - "required": ['keywords'] - } - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step2_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) + p_sum = ', '.join([p.name for p in request.products]) + prompt = r"""Leite aus diesen Produkten 10-25 Keywords für die Wettbewerbsrecherche ab: {products}. Antworte im JSON-Format.""" + schema = {"type": "object", "properties": {"keywords": {"type": "array", "items": {"type": "object", "properties": {"term": {"type": "string"}, "rationale": {"type": "string"}}, "required": ['term', 'rationale']}}}, "required": ['keywords']} + return await call_gemini_json(prompt.format(products=p_sum), schema) @app.post("/api/fetchStep3Data") async def fetch_step3_data(request: FetchStep3DataRequest): - prompts = { - "de": f""" - Rolle: Research-Agent. - Aufgabe: Finde relevante Wettbewerber basierend auf den folgenden Keywords. Fokussiere dich auf den Markt: {request.market_scope}. - Keywords: {', '.join([k.term for k in request.keywords])} - Regeln: - 1. Suche nach Software-Anbietern, nicht nach Resellern, Beratungen oder Implementierungspartnern. - 2. Gib für jeden Kandidaten an: Name, URL, Eignungsbegründung (why), Confidence-Score (0.0-1.0) und Belege (URL+Snippet). - 3. Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Role: Research Agent. - Task: Find relevant competitors based on the following keywords. Focus on the market: {request.market_scope}. - Keywords: {', '.join([k.term for k in request.keywords])} - Rules: - 1. Search for software vendors, not resellers, consultants, or implementation partners. - 2. For each candidate, provide: Name, URL, justification for inclusion (why), confidence score (0.0-1.0), and evidence (URL+snippet). - 3. Respond exclusively in JSON format. - """ - } - - response_schema = { - "type": "object", - "properties": { - "competitor_candidates": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "url": {"type": "string"}, - "confidence": {"type": "number"}, - "why": {"type": "string"}, - "evidence": {"type": "array", "items": evidence_schema} - }, - "required": ['name', 'url', 'confidence', 'why', 'evidence'] - } - } - }, - "required": ['competitor_candidates'] - } - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step3_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) + k_sum = ', '.join([k.term for k in request.keywords]) + prompt = r"""Finde Wettbewerber für Markt {scope} basierend auf: {keywords}. Antworte JSON.""" + schema = {"type": "object", "properties": {"competitor_candidates": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}, "confidence": {"type": "number"}, "why": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'url', 'confidence', 'why', 'evidence']}}}, "required": ['competitor_candidates']} + return await call_gemini_json(prompt.format(scope=request.market_scope, keywords=k_sum), schema) @app.post("/api/fetchStep4Data") async def fetch_step4_data(request: FetchStep4DataRequest): - competitors_summary = '\n'.join([f'- {c.name}: {c.url}' for c in request.competitors]) - prompts = { - "de": f""" - Rolle: Research-Agent. - Aufgabe: Führe eine detaillierte Portfolio- & Positionierungsanalyse für jeden der folgenden Wettbewerber durch. Vergleiche sie mit dem Ausgangsunternehmen {request.company.name} ({request.company.start_url}). - Wettbewerber: - {competitors_summary} - Analyse-Punkte pro Wettbewerber: - 1. portfolio: Kernprodukte (max. 5) mit kurzem Zweck. - 2. target_industries: Hauptzielbranchen. - 3. delivery_model: Geschäfts-/Bereitstellungsmodell (z.B. SaaS, On-Premise), falls ersichtlich. - 4. overlap_score: 0-100, basierend auf Produktfunktion, Zielbranche, Terminologie im Vergleich zum Ausgangsunternehmen. - 5. differentiators: 3-5 Bulletpoints zu Alleinstellungsmerkmalen oder Unterschieden. - 6. evidence: Wichtige Beleg-URLs mit Snippets. - Regeln: - 1. Jede Behauptung mit Quellen belegen. - 2. Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Role: Research Agent. - Task: Conduct a detailed portfolio & positioning analysis for each of the following competitors. Compare them with the initial company {request.company.name} ({request.company.start_url}). - Competitors: - {competitors_summary} - Analysis points per competitor: - 1. portfolio: Core products (max 5) with a brief purpose. - 2. target_industries: Main target industries. - 3. delivery_model: Business/delivery model (e.g., SaaS, On-Premise), if apparent. - 4. overlap_score: 0-100, based on product function, target industry, terminology compared to the initial company. - 5. differentiators: 3-5 bullet points on unique selling points or differences. - 6. evidence: Key supporting URLs with snippets. - Rules: - 1. Back up every claim with sources. - 2. Respond exclusively in JSON format. - """ - } - - response_schema = { - "type": "object", - "properties": { - "analyses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "competitor": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}}} - "portfolio": {"type": "array", "items": {"type": "object", "properties": {"product": {"type": "string"}, "purpose": {"type": "string"}}}} - "target_industries": {"type": "array", "items": {"type": "string"}}, - "delivery_model": {"type": "string"}, - "overlap_score": {"type": "integer"}, - "differentiators": {"type": "array", "items": {"type": "string"}}, - "evidence": {"type": "array", "items": evidence_schema} - }, - "required": ['competitor', 'portfolio', 'target_industries', 'delivery_model', 'overlap_score', 'differentiators', 'evidence'] - } - } - }, - "required": ['analyses'] - } - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step4_data: {e}") - raise HTTPException(status_code=500, detail=str(e)) + c_sum = '\n'.join([f'- {c.name}: {c.url}' for c in request.competitors]) + prompt = r"""Analysiere Portfolio & Positionierung für:\n{comps}\nVergleiche mit {me}. Antworte JSON.""" + schema = {"type": "object", "properties": {"analyses": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}}}, "portfolio": {"type": "array", "items": {"type": "object", "properties": {"product": {"type": "string"}, "purpose": {"type": "string"}}}}, "target_industries": {"type": "array", "items": {"type": "string"}}, "delivery_model": {"type": "string"}, "overlap_score": {"type": "integer"}, "differentiators": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['competitor', 'portfolio', 'target_industries', 'delivery_model', 'overlap_score', 'differentiators', 'evidence']}}}, "required": ['analyses']} + return await call_gemini_json(prompt.format(comps=c_sum, me=request.company.name), schema) @app.post("/api/fetchStep5Data_SilverBullets") async def fetch_step5_data_silver_bullets(request: FetchStep5DataSilverBulletsRequest): - competitor_data_summary = '\n'.join([ - f"- Competitor: {a.competitor['name']}\n - Portfolio Focus: {', '.join([p['product'] for p in a.portfolio]) or 'N/A'}\n - Differentiators: {'; '.join(a.differentiators)}" - for a in request.analyses - ]) - - prompts = { - "de": f""" - Rolle: Strategieberater. - Aufgabe: Erstelle für das Unternehmen "{request.company.name}" eine "Silver Bullet" für jeden Wettbewerber. Eine "Silver Bullet" ist ein prägnanter Satz (max. 25 Wörter), der im Vertriebsgespräch genutzt werden kann, um sich vom jeweiligen Wettbewerber abzugrenzen. - Kontext: {request.company.name} wird mit den folgenden Wettbewerbern verglichen: - {competitor_data_summary.replace("Competitor:", "Wettbewerber:").replace("Portfolio Focus:", "Portfolio-Fokus:").replace("Differentiators:", "Alleinstellungsmerkmale:")} - - Regeln: - 1. Formuliere für JEDEN Wettbewerber einen einzigen, schlagkräftigen Satz. - 2. Der Satz soll eine Schwäche des Wettbewerbers oder eine Stärke von {request.company.name} im direkten Vergleich hervorheben. - 3. Sei prägnant und überzeugend. - 4. Antworte ausschließlich im JSON-Format. - """, - "en": f""" - Role: Strategy Consultant. - Task: Create a "Silver Bullet" for the company "{request.company.name}" for each competitor. A "Silver Bullet" is a concise sentence (max 25 words) that can be used in a sales pitch to differentiate from the respective competitor. - Context: {request.company.name} is being compared with the following competitors: - {competitor_data_summary} - - Rules: - 1. Formulate a single, powerful sentence for EACH competitor. - 2. The sentence should highlight a weakness of the competitor or a strength of {request.company.name} in direct comparison. - 3. Be concise and persuasive. - 4. Respond exclusively in JSON format. - """ - } - - response_schema = { - "type": "object", - "properties": { - "silver_bullets": { - "type": "array", - "items": { - "type": "object", - "properties": { - "competitor_name": {"type": "string"}, - "statement": {"type": "string"} - }, - "required": ['competitor_name', 'statement'] - } - } - }, - "required": ['silver_bullets'] - } - - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step5_data_silver_bullets: {e}") - raise HTTPException(status_code=500, detail=str(e)) + c_sum = '\n'.join([f"- {a.competitor['name']}: {'; '.join(a.differentiators)}" for a in request.analyses]) + prompt = r"""Erstelle prägnante Silver Bullets für {me} gegen diese Wettbewerber:\n{comps}\nAntworte JSON.""" + schema = {"type": "object", "properties": {"silver_bullets": {"type": "array", "items": {"type": "object", "properties": {"competitor_name": {"type": "string"}, "statement": {"type": "string"}}, "required": ['competitor_name', 'statement']}}}, "required": ['silver_bullets']} + return await call_gemini_json(prompt.format(me=request.company.name, comps=c_sum), schema) @app.post("/api/fetchStep6Data_Conclusion") async def fetch_step6_data_conclusion(request: FetchStep6DataConclusionRequest): - competitor_data_summary = '\n\n'.join([ - f"- Competitor: {a.competitor['name']}\n - Portfolio: {', '.join([p['product'] for p in a.portfolio]) or 'N/A'}\n - Target Industries: {', '.join(a.target_industries) or 'N/A'}\n - Overlap Score: {a.overlap_score}\n - Differentiators: {'; '.join(a.differentiators)}" - for a in request.analyses - ]) - - silver_bullets_summary = '\n'.join([f"- Gegen {sb.competitor_name}: \"{sb.statement}\"" for sb in request.silver_bullets]) - if request.language == 'en': - silver_bullets_summary = '\n'.join([f"- Against {sb.competitor_name}: \"{sb.statement}\"" for sb in request.silver_bullets]) - - prompts = { - "de": """ - Rolle: Research-Agent. - Aufgabe: Erstelle ein Fazit der Wettbewerbsanalyse für {company_name}. - - Ausgangsunternehmen ({company_name}) Daten: - - Produkte: {products_summary} - - Branchen: {industries_summary} - - Zusammengefasste Wettbewerber-Daten: - {competitor_data_summary_de} - - Strategische Positionierung ("Silver Bullets"): - {silver_bullets_summary_de} - - Erstelle: - 1. product_matrix: Analysiere ALLE Produkte von {company_name} und den Wettbewerbern. Identifiziere 5-10 generische Produktkategorien oder Kernfunktionalitäten (z.B. "Mobile Lösung", "Disposition & Planung", "Asset Management"). Erstelle dann eine Matrix basierend auf diesen generischen Kategorien. Jedes Element im Array repräsentiert eine dieser Kategorien. Jedes Element hat ein "product" (string, der Name der generischen Kategorie) und ein "availability" (Array). Das "availability"-Array enthält Objekte mit "competitor" (string) und "has_offering" (boolean), für JEDEN Anbieter (inkl. {company_name}), das anzeigt, ob der Anbieter eine Lösung in dieser Kategorie hat. - 2. industry_matrix: Ein Array. Jedes Element repräsentiert eine Branche (von ALLEN Anbietern, inkl. {company_name}). Jedes Element hat eine "industry" (string) und ein "availability" (Array). Das "availability"-Array enthält Objekte mit "competitor" (string) und "has_offering" (boolean), für JEDEN Anbieter (inkl. {company_name}). - 3. overlap_scores: Eine Liste der Wettbewerber und ihrer Overlap-Scores. - 4. summary: Eine knappe Einordnung (2-3 Sätze), wer sich worauf positioniert. - 5. opportunities: Wo liegen Lücken oder Chancen für {company_name}? - 6. next_questions: Max. 5 offene Fragen oder nächste Schritte für den User. - Regeln: - 1. Antworte ausschließlich im JSON-Format gemäß dem vorgegebenen Schema. - ".format( - company_name=request.company.name, - products_summary=', '.join([p.name for p in request.products]), - industries_summary=', '.join([i.name for i in request.industries]), - competitor_data_summary_de=competitor_data_summary.replace("Competitor:", "Wettbewerber:").replace("Portfolio:", "Portfolio:").replace("Target Industries:", "Zielbranchen:").replace("Overlap Score:", "Overlap Score:").replace("Differentiators:", "Alleinstellungsmerkmale:"), - silver_bullets_summary_de=silver_bullets_summary - ), - "en": """ - Role: Research Agent. - Task: Create a conclusion for the competitive analysis for {company_name}. - - Initial Company ({company_name}) Data: - - Products: {products_summary} - - Industries: {industries_summary} - - Summarized Competitor Data: - {competitor_data_summary_en} - - Strategic Positioning ("Silver Bullets"): - {silver_bullets_summary_en} - - Create: - 1. product_matrix: Analyze ALL products from {company_name} and the competitors. Identify 5-10 generic product categories or core functionalities (e.g., "Mobile Solution", "Dispatch & Planning", "Asset Management"). Then create a matrix based on these generic categories. Each element in the array represents one of these categories. Each element has a "product" (string, the name of the generic category) and an "availability" (array). The "availability"-array contains objects with "competitor" (string) and "has_offering" (boolean), for EVERY provider (incl. {company_name}), indicating if the provider has a solution in this category. - 2. industry_matrix: An array. Each element represents an industry (from ALL providers, incl. {company_name}). Each element has an "industry" (string) and an "availability" (array). The "availability"-array contains objects with "competitor" (string) and "has_offering" (boolean), for EVERY provider (incl. {company_name}). - 3. overlap_scores: A list of competitors and their overlap scores. - 4. summary: A brief assessment (2-3 sentences) of who is positioned where. - 5. opportunities: Where are the gaps or opportunities for {company_name}? - 6. next_questions: Max 5 open questions or next steps for the user. - Rules: - 1. Respond exclusively in JSON format according to the provided schema. - ".format( - company_name=request.company.name, - products_summary=', '.join([p.name for p in request.products]), - industries_summary=', '.join([i.name for i in request.industries]), - competitor_data_summary_en=competitor_data_summary, - silver_bullets_summary_en=silver_bullets_summary - ) - } - - response_schema = { - "type": "object", - "properties": { - "conclusion": { - "type": "object", - "properties": { - "product_matrix": { - "type": "array", - "description": "Array representing a feature-based product comparison. Each item is a generic product category or core functionality.", - "items": { - "type": "object", - "properties": { - "product": {"type": "string", "description": "Name of the generic product category/feature."}, - "availability": { - "type": "array", - "description": "Which competitors offer this product.", - "items": { - "type": "object", - "properties": { - "competitor": {"type": "string"}, - "has_offering": {"type": "boolean", "description": "True if the competitor has a similar offering."} - }, - "required": ['competitor', 'has_offering'] - } - } - }, - "required": ['product', 'availability'] - } - }, - "industry_matrix": { - "type": "array", - "description": "Array representing industry comparison. Each item is an industry.", - "items": { - "type": "object", - "properties": { - "industry": {"type": "string", "description": "Name of the industry."}, - "availability": { - "type": "array", - "description": "Which competitors serve this industry.", - "items": { - "type": "object", - "properties": { - "competitor": {"type": "string"}, - "has_offering": {"type": "boolean", "description": "True if the competitor serves this industry."} - }, - "required": ['competitor', 'has_offering'] - } - } - }, - "required": ['industry', 'availability'] - } - }, - "overlap_scores": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "string"}, "score": {"type": "number"}}}}, - "summary": {"type": "string"}, - "opportunities": {"type": "string"}, - "next_questions": {"type": "array", "items": {"type": "string"}} - }, - "required": ['product_matrix', 'industry_matrix', 'overlap_scores', 'summary', 'opportunities', 'next_questions'] - } - }, - "required": ['conclusion'] - } - - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step6_data_conclusion: {e}") - raise HTTPException(status_code=500, detail=str(e)) + prompt = r"""Erstelle ein abschließendes Fazit der Wettbewerbsanalyse für {me}. Antworte JSON.""" + schema = {"type": "object", "properties": {"conclusion": {"type": "object", "properties": {"product_matrix": {"type": "array", "items": {"type": "object", "properties": {"product": {"type": "string"}, "availability": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "string"}, "has_offering": {"type": "boolean"}}, "required": ['competitor', 'has_offering']}}}, "required": ['product', 'availability']}}, "industry_matrix": {"type": "array", "items": {"type": "object", "properties": {"industry": {"type": "string"}, "availability": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "string"}, "has_offering": {"type": "boolean"}}, "required": ['competitor', 'has_offering']}}}, "required": ['industry', 'availability']}}, "overlap_scores": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "string"}, "score": {"type": "number"}}}}, "summary": {"type": "string"}, "opportunities": {"type": "string"}, "next_questions": {"type": "array", "items": {"type": "string"}}}, "required": ['product_matrix', 'industry_matrix', 'overlap_scores', 'summary', 'opportunities', 'next_questions']}}, "required": ['conclusion']} + return await call_gemini_json(prompt.format(me=request.company.name), schema) @app.post("/api/fetchStep7Data_Battlecards") async def fetch_step7_data_battlecards(request: FetchStep7DataBattlecardsRequest): - competitor_data_summary = '\n\n'.join([ - f"- Competitor: {a.competitor['name']}\n - Portfolio Focus: {', '.join([p['product'] for p in a.portfolio]) or 'N/A'}\n - Target Industries: {', '.join(a.target_industries)}\n - Differentiators: {'; '.join(a.differentiators)}\n - Silver Bullet against this competitor: \"{next((sb.statement for sb in request.silver_bullets if sb.competitor_name == a.competitor['name']), 'Not found.')}\"" - for a in request.analyses - ]) - - prompts = { - "de": f""" - Rolle: Vertriebsstratege und Coach für das B2B-Softwareunternehmen "{request.company.name}". - Aufgabe: Erstelle für jeden der folgenden Wettbewerber eine detaillierte "Sales Battlecard". Diese Battlecard soll dem Vertriebsteam konkrete, handlungsorientierte Argumente für Kundengespräche an die Hand geben. - - Informationen über das eigene Unternehmen: {request.company.name} - Detaillierte Analyse der Wettbewerber: - {competitor_data_summary.replace("Competitor:", "Wettbewerber:").replace("Portfolio Focus:", "Portfolio-Fokus:").replace("Target Industries:", "Zielbranchen:").replace("Differentiators:", "Alleinstellungsmerkmale:").replace("Silver Bullet against this competitor:", "Silver Bullet gegen diesen Wettbewerber:")} - - Für JEDEN Wettbewerber, erstelle die folgenden Sektionen einer Battlecard im JSON-Format: - 1. **competitor_name**: Der Name des Wettbewerbers. - 2. **competitor_profile**: - * **focus**: Fasse den Kernfokus des Wettbewerbers (Produkte & Branchen) in einem Satz zusammen. - * **positioning**: Beschreibe die Kernpositionierung des Wettbewerbers in 1-2 Sätzen. - 3. **strengths_vs_weaknesses**: Formuliere 3-4 prägnante Stichpunkte. Jeder Stichpunkt soll eine Stärke von "{request.company.name}" einer vermuteten Schwäche des Wettbewerbers gegenüberstellen. Beginne die Sätze z.B. mit "Während [Wettbewerber]..., bieten wir...". - 4. **landmine_questions**: Formuliere 3-5 intelligente, offene "Landminen"-Fragen, die ein Vertriebsmitarbeiter einem potenziellen Kunden stellen kann. Diese Fragen sollen die Schwächen des Wettbewerbers aufdecken oder die Stärken von "{request.company.name}" betonen, ohne den Wettbewerber direkt anzugreifen. - 5. **silver_bullet**: Übernimm die bereits formulierte "Silver Bullet" für diesen Wettbewerber. - - Regeln: - - Sei präzise, überzeugend und nutze eine aktive, vertriebsorientierte Sprache. - - Die "landmine_questions" müssen so formuliert sein, dass sie den Kunden zum Nachdenken anregen und ihn in Richtung der Vorteile von "{request.company.name}" lenken. - - Antworte ausschließlich im JSON-Format gemäß dem vorgegebenen Schema für ein Array von Battlecards. - ", - "en": f""" - Role: Sales Strategist and Coach for the B2B software company "{request.company.name}". - Task: Create a detailed "Sales Battlecard" for each of the following competitors. This battlecard should provide the sales team with concrete, actionable arguments for customer conversations. - - Information about our own company: {request.company.name} - Detailed analysis of competitors: - {competitor_data_summary} - - For EACH competitor, create the following sections of a battlecard in JSON format: - 1. **competitor_name**: The name of the competitor. - 2. **competitor_profile**: - * **focus**: Summarize the competitor's core focus (products & industries) in one sentence. - * **positioning**: Describe the competitor's core positioning in 1-2 sentences. - 3. **strengths_vs_weaknesses**: Formulate 3-4 concise bullet points. Each point should contrast a strength of "{request.company.name}" with a presumed weakness of the competitor. Start sentences with, for example, "While [Competitor]..., we offer...". - 4. **landmine_questions**: Formulate 3-5 intelligent, open-ended "landmine" questions that a sales representative can ask a potential customer. These questions should uncover the competitor's weaknesses or emphasize the strengths of "{request.company.name}" without attacking the competitor directly. - 5. **silver_bullet**: Use the "Silver Bullet" already formulated for this competitor. - - Rules: - - Be precise, persuasive, and use active, sales-oriented language. - - The "landmine_questions" must be formulated to make the customer think and guide them towards the advantages of "{request.company.name}". - - Respond exclusively in JSON format according to the specified schema for an array of battlecards. - " - } - - - response_schema = { - "type": "object", - "properties": { - "battlecards": { - "type": "array", - "items": { - "type": "object", - "properties": { - "competitor_name": {"type": "string"}, - "competitor_profile": { - "type": "object", - "properties": { - "focus": {"type": "string"}, - "positioning": {"type": "string"} - }, - "required": ['focus', 'positioning'] - }, - "strengths_vs_weaknesses": {"type": "array", "items": {"type": "string"}}, - "landmine_questions": {"type": "array", "items": {"type": "string"}}, - "silver_bullet": {"type": "string"} - }, - "required": ['competitor_name', 'competitor_profile', 'strengths_vs_weaknesses', 'landmine_questions', 'silver_bullet'] - } - } - }, - "required": ['battlecards'] - } - - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - return parse_json_response(response.text) - except Exception as e: - print(f"Error in fetch_step7_data_battlecards: {e}") - raise HTTPException(status_code=500, detail=str(e)) + prompt = r"""Erstelle Sales Battlecards für {me} gegen seine Wettbewerber. Antworte JSON.""" + schema = {"type": "object", "properties": {"battlecards": {"type": "array", "items": {"type": "object", "properties": {"competitor_name": {"type": "string"}, "competitor_profile": {"type": "object", "properties": {"focus": {"type": "string"}, "positioning": {"type": "string"}}, "required": ['focus', 'positioning']}, "strengths_vs_weaknesses": {"type": "array", "items": {"type": "string"}}, "landmine_questions": {"type": "array", "items": {"type": "string"}}, "silver_bullet": {"type": "string"}}, "required": ['competitor_name', 'competitor_profile', 'strengths_vs_weaknesses', 'landmine_questions', 'silver_bullet']}}}, "required": ['battlecards']} + return await call_gemini_json(prompt.format(me=request.company.name), schema) @app.post("/api/fetchStep8Data_ReferenceAnalysis") async def fetch_step8_data_reference_analysis(request: FetchStep8DataReferenceAnalysisRequest): - competitors_summary = '\n'.join([f'- {c.name}: {c.url}' for c in request.competitors]) - - prompts = { - "de": """ - Rolle: Faktentreuer Research-Agent. Deine Antworten MÜSSEN ausschließlich auf den Ergebnissen der von dir durchgeführten Websuche basieren. - Aufgabe: Führe für jeden der folgenden Wettbewerber eine Websuche durch, um deren offizielle Referenzkunden, Case Studies oder Success Stories zu finden. - Wettbewerber: - {competitors_summary_de} - - ABLAUF FÜR JEDEN WETTBEWERBER: - 1. **SUCHE**: Führe eine gezielte Suche durch mit Phrasen wie "[Wettbewerber-Name] Referenzen", "[Wettbewerber-Name] Case Studies", "[Wettbewerber-Name] Kunden". - 2. **VALIDIERUNG**: Analysiere die Suchergebnisse. Konzentriere dich AUSSCHLIESSLICH auf Links, die zur offiziellen Domain des Wettbewerbers gehören (z.B. `wettbewerber.com/referenzen`). Ignoriere Pressemitteilungen auf Drittseiten, Partnerlisten oder Nachrichtenartikel. - 3. **EXTRAKTION**: Extrahiere die geforderten Informationen NUR von diesen validierten, offiziellen Seiten. - - SEHR WICHTIGE REGELN ZUR VERMEIDUNG VON FALSCHINFORMATIONEN: - - **NUR GEFUNDENE DATEN**: Gib NUR Kunden an, für die du eine dedizierte Case-Study-Seite oder einen klaren Testimonial-Abschnitt auf der OFFIZIELLEN Website des Wettbewerbers gefunden hast. - - **KEINE HALLUZINATION**: Erfinde KEINE Kunden, Branchen oder Zitate. Wenn du für einen Wettbewerber absolut nichts findest, gib ein leeres "references" Array zurück. Dies ist besser als falsche Informationen. - - **DIREKTER LINK**: Die 'case_study_url' MUSS der exakte, funktionierende Link zur Seite sein, auf der die Informationen gefunden wurden. - - Antworte AUSSCHLIESSLICH im JSON-Format, eingeschlossen in einem Markdown-Codeblock. - Extrahiere für JEDEN GEFUNDENEN UND VERIFIZIERTEN Referenzkunden (max. 5 pro Wettbewerber) die geforderten Felder. - ".format( - competitors_summary_de=competitors_summary - ), - "en": """ - Role: Fact-based Research Agent. Your answers MUST be based solely on the results of the web search you perform. - Task: For each of the following competitors, conduct a web search to find their official reference customers, case studies, or success stories. - Competitors: - {competitors_summary_en} - - PROCESS FOR EACH COMPETITOR: - 1. **SEARCH**: Conduct a targeted search with phrases like "[Competitor Name] references", "[Competitor Name] case studies", "[Competitor Name] customers". - 2. **VALIDATION**: Analyze the search results. Focus EXCLUSIVELY on links that belong to the competitor's official domain (e.g., `competitor.com/references`). Ignore press releases on third-party sites, partner lists, or news articles. - 3. **EXTRACTION**: Extract the required information ONLY from these validated, official pages. - - VERY IMPORTANT RULES TO AVOID MISINFORMATION: - - **ONLY FOUND DATA**: ONLY list customers for whom you have found a dedicated case study page or a clear testimonial section on the OFFICIAL website of the competitor. - - **NO HALLUCINATION**: DO NOT invent customers, industries, or quotes. If you find absolutely nothing for a competitor, return an empty "references" array. This is better than false information. - - **DIRECT LINK**: The 'case_study_url' MUST be the exact, working link to the page where the information was found. - - Respond EXCLUSIVELY in JSON format, enclosed in a markdown code block. - For EACH FOUND AND VERIFIED reference customer (max 5 per competitor), extract the required fields. - ".format( - competitors_summary_en=competitors_summary + c_sum = '\n'.join([f'- {c.name}: {c.url}' for c in request.competitors]) + prompt = r"""Finde offizielle Referenzkunden für diese Wettbewerber:\n{comps}\nAntworte JSON.""" + schema = {"type": "object", "properties": {"reference_analysis": {"type": "array", "items": {"type": "object", "properties": {"competitor_name": {"type": "string"}, "references": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "industry": {"type": "string"}, "testimonial_snippet": {"type": "string"}, "case_study_url": {"type": "string"}}, "required": ["name", "industry", "testimonial_snippet", "case_study_url"]}}}, "required": ["competitor_name", "references"]}}}, "required": ["reference_analysis"]} + # IMPORTANT: The new SDK supports tools via a list in config, not directly as args to generate_content. + response = client.models.generate_content( + model=MODEL_NAME, + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type='application/json', + tools=[types.Tool(google_search_retrieval={})] ) - } - - response_schema = { - "type": "object", - "properties": { - "reference_analysis": { - "type": "array", - "items": { - "type": "object", - "properties": { - "competitor_name": {"type": "string"}, - "references": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "industry": {"type": "string"}, - "testimonial_snippet": {"type": "string"}, - "case_study_url": {"type": "string"} - }, - "required": ["name", "industry", "testimonial_snippet", "case_study_url"] - } - } - }, - "required": ["competitor_name", "references"] - } - } - }, - "required": ['reference_analysis'] - } + ) + return parse_json_response(response) - try: - response = await model.generate_content_async( - prompts[request.language], - generation_config=genai.GenerationConfig( - response_mime_type="application/json", - response_schema=response_schema - ), - tools=[genai.types.Tool(google_search_retrieval=genai.types.GoogleSearchRetrieval())], # Correct way to enable search in Python SDK - safety_settings={ - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, - }, - ) - grounding_metadata = [chunk.to_dict() for chunk in response.candidates[0].grounding_metadata.grounding_chunks] if response.candidates[0].grounding_metadata else [] - parsed_data = parse_json_response(response.text) - return {**parsed_data, "groundingMetadata": grounding_metadata} - except Exception as e: - print(f"Error in fetch_step8_data_reference_analysis: {e}") - raise HTTPException(status_code=500, detail=str(e)) +# Static Files +dist_path = os.path.join(os.getcwd(), "dist") +if os.path.exists(dist_path): + print(f"DEBUG: Mounting static files from {dist_path}") + app.mount("/", StaticFiles(directory=dist_path, html=True), name="static") -# Mount static files AFTER all API routes -if os.path.exists("dist"): - app.mount("/", StaticFiles(directory="dist", html=True), name="static") +@app.get("/api/health") +async def health_check(): + return {"status": "ok", "sdk": "modern-genai", "model": MODEL_NAME} if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/competitor-analysis-app/requirements.txt b/competitor-analysis-app/requirements.txt index 4fec3020..bde72044 100644 --- a/competitor-analysis-app/requirements.txt +++ b/competitor-analysis-app/requirements.txt @@ -1,7 +1,5 @@ fastapi==0.104.1 uvicorn==0.24.0.post1 python-dotenv==1.0.0 -google-generativeai==0.3.0 -# The frontend uses jspdf and jspdf-autotable, but these are JS libraries, not Python. No direct Python equivalent is needed unless PDF generation moves to backend. -# google-genai (newer SDK) might be needed if using Imagen 4 or Gemini 2.x features. -# Pillow for image processing. +google-genai +# The frontend uses jspdf and jspdf-autotable, but these are JS libraries, not Python. \ No newline at end of file diff --git a/dashboard/index.html b/dashboard/index.html index 0389933c..6faaa9d4 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -163,6 +163,16 @@ Starten → + + +
+ Analysieren Sie Wettbewerber, erstellen Sie Battlecards und finden Sie "Silver Bullets" für den Vertrieb. +
+ Starten → +