Spaces:
Sleeping
Sleeping
| from flask import Flask, request, jsonify, send_from_directory | |
| from flask_cors import CORS | |
| from crewai import Agent, Task, Crew, LLM | |
| from crewai.process import Process | |
| from crewai_tools import SerperDevTool | |
| import os | |
| import json | |
| import time | |
| import fcntl | |
| from datetime import datetime | |
| # ββ Load .env only in local development ββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except ImportError: | |
| pass | |
| app = Flask(__name__) | |
| CORS(app) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # API KEYS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| SERPER_API_KEY = os.getenv("SERPER_API_KEY") | |
| if not GEMINI_API_KEY or not SERPER_API_KEY: | |
| raise EnvironmentError( | |
| "Missing required environment variables. " | |
| "Set GEMINI_API_KEY and SERPER_API_KEY in the environment or .env." | |
| ) | |
| os.environ["SERPER_API_KEY"] = SERPER_API_KEY | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODEL INITIALISATION | |
| # | |
| # lite_llm β gemini-2.0-flash-lite fast Β· high RPM Β· low complexity | |
| # pro_llm β gemini-2.5-flash quality Β· reserved for final work | |
| # search_llm β gemini-2.0-flash-lite used for standalone search agent | |
| # (gemma-2-9b-it removed β not reliably available via same key) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lite_llm = LLM( | |
| model="gemini/gemini-2.5-flash-lite", | |
| temperature=0.2, | |
| api_key=GEMINI_API_KEY, | |
| ) | |
| pro_llm = LLM( | |
| model="gemini/gemini-2.5-flash", | |
| temperature=0.3, | |
| api_key=GEMINI_API_KEY, | |
| ) | |
| # Search agent uses lite to preserve pro quota | |
| search_llm = LLM( | |
| model="gemini/gemini-2.5-flash-lite", | |
| temperature=0.1, | |
| api_key=GEMINI_API_KEY, | |
| ) | |
| # ββ Tools βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| search_tool = SerperDevTool() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MEMORY CONFIGURATION | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _resolve_memory_path() -> str: | |
| env_path = os.getenv("MEMORY_PATH") | |
| if env_path: | |
| print(f"[Memory] Using env-specified path: {env_path}") | |
| return env_path | |
| tmp_path = "/tmp/memory.json" | |
| try: | |
| with open(tmp_path, "a", encoding="utf-8") as f: | |
| pass | |
| print(f"[Memory] Using /tmp path: {tmp_path}") | |
| return tmp_path | |
| except IOError: | |
| pass | |
| base_dir = os.path.dirname(os.path.abspath(__file__)) | |
| local_path = os.path.join(base_dir, "memory.json") | |
| print(f"[Memory] Using local path: {local_path}") | |
| return local_path | |
| MEMORY_FILE = _resolve_memory_path() | |
| MAX_MEMORY_ENTRIES = 15 | |
| if not os.path.exists(MEMORY_FILE): | |
| try: | |
| with open(MEMORY_FILE, "w", encoding="utf-8") as f: | |
| json.dump([], f) | |
| print(f"[Memory] Created empty memory file at: {MEMORY_FILE}") | |
| except IOError as e: | |
| print(f"[Memory] WARNING: Could not pre-create memory file: {e}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MEMORY HELPERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_memory() -> list: | |
| if not os.path.exists(MEMORY_FILE): | |
| return [] | |
| try: | |
| with open(MEMORY_FILE, "r", encoding="utf-8") as f: | |
| raw = f.read().strip() | |
| if not raw: | |
| return [] | |
| data = json.loads(raw) | |
| entries = data if isinstance(data, list) else [] | |
| print(f"[Memory] Loaded {len(entries)} entries from {MEMORY_FILE}") | |
| return entries | |
| except json.JSONDecodeError as exc: | |
| print(f"[Memory] JSON corrupt β resetting. Reason: {exc}") | |
| return [] | |
| except IOError as exc: | |
| print(f"[Memory] Cannot read {MEMORY_FILE}: {exc}") | |
| return [] | |
| def save_memory(entry: dict) -> bool: | |
| print(f"[Memory] Saving entry for company: {entry.get('company', 'unknown')}") | |
| if not isinstance(entry, dict) or not entry: | |
| print("[Memory] ERROR: entry is empty or not a dict β skipping.") | |
| return False | |
| try: | |
| parent = os.path.dirname(MEMORY_FILE) | |
| if parent and not os.path.exists(parent): | |
| os.makedirs(parent, exist_ok=True) | |
| memory = load_memory() | |
| memory.append(entry) | |
| if len(memory) > MAX_MEMORY_ENTRIES: | |
| memory = memory[-MAX_MEMORY_ENTRIES:] | |
| tmp_file = MEMORY_FILE + ".tmp" | |
| with open(tmp_file, "w", encoding="utf-8") as f: | |
| try: | |
| fcntl.flock(f, fcntl.LOCK_EX) | |
| except Exception: | |
| pass | |
| json.dump(memory, f, indent=2, ensure_ascii=False) | |
| f.flush() | |
| os.fsync(f.fileno()) | |
| try: | |
| fcntl.flock(f, fcntl.LOCK_UN) | |
| except Exception: | |
| pass | |
| os.replace(tmp_file, MEMORY_FILE) | |
| verify = load_memory() | |
| if len(verify) > 0: | |
| print(f"[Memory] β Saved. Total entries now: {len(verify)}") | |
| return True | |
| else: | |
| print("[Memory] β Write seemed OK but file is empty on verify!") | |
| return False | |
| except PermissionError as exc: | |
| print(f"[Memory] β PERMISSION DENIED: {MEMORY_FILE} β {exc}") | |
| return False | |
| except IOError as exc: | |
| print(f"[Memory] β IOError writing to {MEMORY_FILE}: {exc}") | |
| return False | |
| except Exception as exc: | |
| print(f"[Memory] β Unexpected: {type(exc).__name__}: {exc}") | |
| return False | |
| def get_recent_memory(n: int = 3) -> list: | |
| entries = load_memory() | |
| recent = entries[-n:] | |
| print(f"[Memory] Returning {len(recent)} recent entries.") | |
| return recent | |
| def format_memory_context(entries: list) -> str: | |
| if not entries: | |
| return "No prior meeting history available." | |
| parts = [] | |
| for i, e in enumerate(entries, 1): | |
| ts = e.get("timestamp", "unknown time") | |
| company = e.get("company", "N/A") | |
| objective = e.get("objective", "N/A") | |
| summary = e.get("summary", "N/A") | |
| parts.append( | |
| f"[Past Meeting {i} | {ts}]\n" | |
| f" Company : {company}\n" | |
| f" Objective : {objective}\n" | |
| f" Takeaway : {summary}" | |
| ) | |
| return "\n\n".join(parts) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # DECISION PARSER | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def parse_decision(text: str) -> dict: | |
| upper = text.upper() | |
| if "SEARCH: ALWAYS" in upper: | |
| search_mode = "ALWAYS" | |
| elif "SEARCH: LIGHT" in upper: | |
| search_mode = "LIGHT" | |
| else: | |
| search_mode = "MINIMAL" | |
| use_search = search_mode != "MINIMAL" | |
| use_memory = "MEMORY: NO" not in upper | |
| if "PRIORITY: INDUSTRY" in upper: | |
| priority = "Industry" | |
| elif "PRIORITY: STRATEGY" in upper: | |
| priority = "Strategy" | |
| else: | |
| priority = "Context" | |
| if "DEPTH: DEEP" in upper: | |
| depth = "DEEP" | |
| elif "DEPTH: SHORT" in upper: | |
| depth = "SHORT" | |
| else: | |
| depth = "NORMAL" | |
| return { | |
| "use_search": use_search, | |
| "search_mode": search_mode, | |
| "use_memory": use_memory, | |
| "priority": priority, | |
| "depth": depth, | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SMART MODEL ROUTER β rate-limit detection + fallback | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _RATE_LIMIT_KEYWORDS = [ | |
| "429", "rate_limit", "Rate limit", "quota", "Quota", | |
| "RESOURCE_EXHAUSTED", "503", "UNAVAILABLE", "overloaded", | |
| "too many requests", "exceeded", | |
| ] | |
| def _is_rate_error(msg: str) -> bool: | |
| return any(k.lower() in msg.lower() for k in _RATE_LIMIT_KEYWORDS) | |
| def kickoff_with_retry(crew, retries: int = 3, base_wait: int = 12): | |
| for attempt in range(retries): | |
| try: | |
| return crew.kickoff() | |
| except Exception as e: | |
| msg = str(e) | |
| if _is_rate_error(msg) and attempt < retries - 1: | |
| wait = base_wait * (2 ** attempt) | |
| print(f"[Retry] Rate limit hit β waiting {wait}s " | |
| f"(attempt {attempt + 1}/{retries})") | |
| time.sleep(wait) | |
| continue | |
| raise | |
| def kickoff_with_model_fallback(crew_builder_fn, high_quality: bool = False): | |
| primary = pro_llm if high_quality else lite_llm | |
| secondary = lite_llm if high_quality else None | |
| crew = crew_builder_fn(primary) | |
| try: | |
| return kickoff_with_retry(crew) | |
| except Exception as e: | |
| if high_quality and secondary and _is_rate_error(str(e)): | |
| print("[ModelRouter] gemini-2.5-flash rate-limited β " | |
| "falling back to gemini-2.0-flash-lite") | |
| time.sleep(8) | |
| crew = crew_builder_fn(secondary) | |
| return kickoff_with_retry(crew) | |
| raise | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FLASK ROUTES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| return send_from_directory(".", "index.html") | |
| def health(): | |
| return jsonify({ | |
| "status": "ok", | |
| "models": { | |
| "lite": "gemini/gemini-2.5-flash-lite", | |
| "pro": "gemini/gemini-2.5-flash", | |
| "search": "gemini/gemini-2.0-flash-lite", | |
| }, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| }), 200 | |
| def debug_memory(): | |
| test_entry = { | |
| "timestamp": datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"), | |
| "company": "DEBUG_TEST", | |
| "objective": "Verify memory write works", | |
| "summary": "Test entry to confirm save_memory() is functional.", | |
| } | |
| saved = save_memory(test_entry) | |
| loaded = load_memory() | |
| return jsonify({ | |
| "memory_file_path": MEMORY_FILE, | |
| "file_exists": os.path.exists(MEMORY_FILE), | |
| "is_writable": os.access(os.path.dirname(MEMORY_FILE) or ".", os.W_OK), | |
| "save_returned": saved, | |
| "total_entries": len(loaded), | |
| "entries": loaded, | |
| }) | |
| def run_agent(): | |
| data = request.get_json(force=True) | |
| required_fields = [ | |
| "company_name", "meeting_objective", | |
| "attendees", "meeting_duration", "focus_areas", | |
| ] | |
| missing = [f for f in required_fields if not data.get(f)] | |
| if missing: | |
| return jsonify({"error": f"Missing required fields: {', '.join(missing)}"}), 400 | |
| company_name = data["company_name"] | |
| meeting_objective = data["meeting_objective"] | |
| attendees = data["attendees"] | |
| meeting_duration = int(data["meeting_duration"]) | |
| focus_areas = data["focus_areas"] | |
| start_time = time.time() | |
| try: | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PHASE 0 β Load Memory | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| recent_memory = get_recent_memory(3) | |
| memory_context = format_memory_context(recent_memory) | |
| has_memory = len(recent_memory) > 0 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PHASE 1 β Decision Agent (lite_llm) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_decision_crew(llm): | |
| decision_agent = Agent( | |
| role="Meeting Prep Orchestrator", | |
| goal=( | |
| "Decide how the downstream agents should use web search, " | |
| "memory, and which analysis area to prioritize for this meeting." | |
| ), | |
| backstory=( | |
| "You coordinate a pipeline of expert agents. You never guess. " | |
| "You output only a strict decision block that configures search, " | |
| "memory, and analysis depth." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=llm, | |
| ) | |
| decision_task = Task( | |
| description=f""" | |
| You are orchestrating a multi-agent system that prepares executive meeting briefs. | |
| MEETING REQUEST: | |
| - Company : {company_name} | |
| - Objective : {meeting_objective} | |
| - Attendees : {attendees} | |
| - Duration : {meeting_duration} minutes | |
| - Focus areas : {focus_areas} | |
| PAST MEETING MEMORY: | |
| {memory_context} | |
| You must output EXACTLY these 5 lines (no extra text): | |
| SEARCH: ALWAYS or LIGHT or MINIMAL | |
| MEMORY: YES or NO | |
| PRIORITY: Context or Industry or Strategy | |
| DEPTH: SHORT or NORMAL or DEEP | |
| REASONING: one concise sentence explaining all decisions | |
| DECISION RULES: | |
| - SEARCH: | |
| - ALWAYS β new or complex company/industry; need 4β5 web searches | |
| - LIGHT β known company; 2β3 web searches are enough | |
| - MINIMAL β mostly internal topic; 1 search just to validate facts | |
| - MEMORY: | |
| - YES if past memory mentions this company or very similar ones | |
| - NO if memory is empty or clearly unrelated | |
| - PRIORITY: | |
| - Context β understanding the company is the biggest gap | |
| - Industry β market / competition / trends are most important | |
| - Strategy β agenda and talking points need most work | |
| - DEPTH: | |
| - SHORT β meeting_duration β€ 30 minutes | |
| - NORMAL β 31β60 minutes | |
| - DEEP β >60 minutes; brief should be very detailed | |
| """, | |
| agent=decision_agent, | |
| expected_output="Exactly 5 lines: SEARCH, MEMORY, PRIORITY, DEPTH, REASONING.", | |
| ) | |
| return Crew( | |
| agents=[decision_agent], | |
| tasks=[decision_task], | |
| verbose=False, | |
| max_rpm=9, | |
| max_execution_time=45, | |
| process=Process.sequential, | |
| ) | |
| decision_result = kickoff_with_model_fallback(build_decision_crew, high_quality=False) | |
| decision_text = str(decision_result).strip() | |
| decision_flags = parse_decision(decision_text) | |
| use_search = decision_flags["use_search"] | |
| search_mode = decision_flags["search_mode"] | |
| use_memory = decision_flags["use_memory"] and has_memory | |
| priority = decision_flags["priority"] | |
| depth = decision_flags["depth"] | |
| # ββ Memory injection ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| memory_injection = ( | |
| f""" | |
| PRIOR MEETING CONTEXT (MANDATORY TO CONSIDER): | |
| {memory_context} | |
| You MUST: | |
| - Reuse relevant insights from past meetings where helpful. | |
| - Maintain continuity in recommendations. | |
| - Avoid contradicting past decisions unless clearly justified. | |
| """ | |
| if use_memory else "" | |
| ) | |
| # ββ Tool assignments per agent ββββββββββββββββββββββββββββββββββββββββ | |
| context_tools = [search_tool] if use_search else [] | |
| industry_tools = [search_tool] if (use_search and priority == "Industry") else [] | |
| # ββ Search instructions based on mode ββββββββββββββββββββββββββββββββ | |
| if search_mode == "ALWAYS": | |
| context_search_instruction = ( | |
| "MANDATORY: Use the search tool 3β5 times for company profile, " | |
| "recent news, products, and competitors." | |
| ) | |
| industry_search_instruction = ( | |
| "MANDATORY: Use the search tool 2β3 times for industry trends, " | |
| "market size, and key competitors." | |
| ) | |
| elif search_mode == "LIGHT": | |
| context_search_instruction = ( | |
| "MANDATORY: Use the search tool at least 2 times for company " | |
| "overview and latest news." | |
| ) | |
| industry_search_instruction = ( | |
| "OPTIONAL: Use the search tool at most 1β2 times if needed." | |
| ) | |
| else: # MINIMAL | |
| context_search_instruction = ( | |
| "MANDATORY: Use the search tool exactly once to validate basic facts." | |
| ) | |
| industry_search_instruction = ( | |
| "Do NOT perform additional web searches; rely on provided context." | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PHASE 2 β Main 4-Agent Crew | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_main_crew(context_llm, industry_llm, strategy_llm, brief_llm): | |
| context_analyzer = Agent( | |
| role="Meeting Context Specialist", | |
| goal="Produce a concise, factual company + meeting context summary.", | |
| backstory=( | |
| "You quickly understand complex business contexts and identify " | |
| "only the most critical, verifiable information. You prefer " | |
| "specific numbers, dates, and named entities over vague statements." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=context_llm, | |
| tools=context_tools, | |
| ) | |
| industry_insights_generator = Agent( | |
| role="Industry Expert", | |
| goal="Provide a short but insightful industry overview and key trends.", | |
| backstory=( | |
| "You are a seasoned industry analyst who spots important trends, " | |
| "competitors, opportunities, and risks. You ground your analysis " | |
| "in concrete facts and examples." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=industry_llm, | |
| tools=industry_tools, | |
| ) | |
| strategy_formulator = Agent( | |
| role="Meeting Strategist", | |
| goal="Design a tight, outcome-focused meeting strategy and agenda.", | |
| backstory=( | |
| "You create practical, time-boxed agendas that align with " | |
| "strategic goals and stakeholder interests. Every recommendation " | |
| "is specific, actionable, and time-bound." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=strategy_llm, | |
| ) | |
| executive_briefing_creator = Agent( | |
| role="Communication Specialist", | |
| goal="Synthesize everything into a clear, actionable executive brief.", | |
| backstory=( | |
| "You distill complex analysis into crisp, high-impact talking points, " | |
| "Q&A prep, and recommendations for C-level executives." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=brief_llm, | |
| ) | |
| context_analysis_task = Task( | |
| description=f""" | |
| You are preparing for a meeting with {company_name}. | |
| Search instruction: {context_search_instruction} | |
| You MUST follow this search instruction exactly if the search tool is available. | |
| Produce a context summary that covers: | |
| - Company snapshot: what they do, scale, geography. | |
| - 1β3 recent notable news items or strategic moves. | |
| - Key products / services relevant to this meeting. | |
| - 3β5 major direct competitors. | |
| Meeting details: | |
| - Objective : {meeting_objective} | |
| - Attendees : {attendees} | |
| - Duration : {meeting_duration} minutes | |
| - Focus areas: {focus_areas} | |
| { "PRIORITY FLAG: Context analysis is the highest priorityβgo deeper here." if priority == "Context" else "" } | |
| {memory_injection} | |
| Requirements: | |
| - Include specific numbers (revenue, employees, etc.) where possible. | |
| - Include dates for major events or news. | |
| - Avoid generic phrases like "leverage synergies" or "move the needle". | |
| - Target length: 400β700 words. | |
| Output style: markdown, with clear headings and bullet points. | |
| """, | |
| agent=context_analyzer, | |
| expected_output="A concise markdown summary of company + meeting context.", | |
| ) | |
| industry_analysis_task = Task( | |
| description=f""" | |
| Based on the previous context analysis for {company_name} and the | |
| meeting objective: "{meeting_objective}", provide an industry-level view. | |
| Search instruction: {industry_search_instruction} | |
| Focus on: | |
| - 3β5 key industry or market trends relevant to this meeting. | |
| - Competitive landscape and where {company_name} roughly fits. | |
| - 3β5 main opportunities {company_name} could pursue. | |
| - 3β5 main risks or threats they should watch. | |
| { "PRIORITY FLAG: Industry analysis is the highest priorityβgo deeper here." if priority == "Industry" else "" } | |
| {memory_injection} | |
| Requirements: | |
| - Use examples of competitors and adjacent players. | |
| - Include any relevant regulations or technology trends. | |
| - Target length: 400β700 words. | |
| Output: markdown with clear headings and bullet points. | |
| """, | |
| agent=industry_insights_generator, | |
| expected_output="A short, insightful markdown industry analysis.", | |
| ) | |
| strategy_development_task = Task( | |
| description=f""" | |
| Using the prior analyses (context + industry), design a concrete strategy | |
| for the {meeting_duration}-minute meeting with {company_name}. | |
| Do NOT perform web searches. | |
| Produce: | |
| 1. A time-boxed agenda (section name + minutes) that sums to {meeting_duration} minutes. | |
| 2. 3β7 key talking points the host should definitely cover. | |
| 3. For each focus area in: "{focus_areas}", propose 1β3 concrete strategies. | |
| { "PRIORITY FLAG: Strategy development is the highest priorityβbe especially detailed and actionable." if priority == "Strategy" else "" } | |
| {memory_injection} | |
| Requirements: | |
| - Every agenda item must have a clear outcome or purpose. | |
| - Every recommendation must specify WHO does WHAT by WHEN. | |
| - Avoid vague language such as "discuss opportunities" or "align on strategy". | |
| - Target length: 600β800 words. | |
| Output: markdown, bullet-point heavy. | |
| """, | |
| agent=strategy_formulator, | |
| expected_output=( | |
| "A succinct markdown meeting strategy with time-boxed agenda and talking points." | |
| ), | |
| ) | |
| executive_brief_task = Task( | |
| description=f""" | |
| Synthesize EVERYTHING into a single executive brief for the meeting | |
| with {company_name}. | |
| You will receive prior analyses (context, industry, strategy). Use them all. | |
| IMPORTANT: Output ONLY the final brief in markdown. | |
| No internal reasoning, planning text, or preamble. | |
| Required structure: | |
| # Executive Summary | |
| - 3β6 bullet points capturing the meeting objective and context. | |
| ## Company & Industry Snapshot | |
| - Short bullets on who {company_name} is and key market dynamics. | |
| ## Meeting Goals & Success Criteria | |
| - 3β5 clearly stated, measurable goals. | |
| - How the host will know the meeting succeeded. | |
| ## Recommended Agenda & Key Talking Points | |
| - Tight recap of the time-boxed agenda (must total {meeting_duration} minutes). | |
| - Bullet list of the most important talking points, tied to data or examples. | |
| ## Anticipated Questions & Prepared Answers | |
| - 5β10 likely questions from attendees based on their roles. | |
| - 1β3 sentence answer for each, grounded in prior analysis. | |
| ## Strategic Recommendations & Next Steps | |
| - 3β5 actionable post-meeting recommendations. | |
| - Suggested next steps and rough timelines. | |
| Requirements: | |
| - Markdown headings + bullets. | |
| - Target length: 900β1300 words. | |
| - Use specific numbers, dates, and names where possible. | |
| - Maintain a professional, concise tone suitable for C-level executives. | |
| """, | |
| agent=executive_briefing_creator, | |
| expected_output=( | |
| "A complete markdown executive brief ready to share before the meeting." | |
| ), | |
| ) | |
| return Crew( | |
| agents=[ | |
| context_analyzer, | |
| industry_insights_generator, | |
| strategy_formulator, | |
| executive_briefing_creator, | |
| ], | |
| tasks=[ | |
| context_analysis_task, | |
| industry_analysis_task, | |
| strategy_development_task, | |
| executive_brief_task, | |
| ], | |
| verbose=False, | |
| max_rpm=4, | |
| max_execution_time=300, | |
| process=Process.sequential, | |
| ) | |
| # ββ Run main crew βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| main_crew = build_main_crew( | |
| context_llm = lite_llm, | |
| industry_llm = lite_llm, | |
| strategy_llm = pro_llm, | |
| brief_llm = pro_llm, | |
| ) | |
| main_result = kickoff_with_retry(main_crew) | |
| except Exception as e: | |
| if _is_rate_error(str(e)): | |
| print("[ModelRouter] gemini-2.5-flash quota hit in main crew β " | |
| "full gemini-2.0-flash-lite fallback") | |
| time.sleep(15) | |
| main_crew = build_main_crew( | |
| context_llm = lite_llm, | |
| industry_llm = lite_llm, | |
| strategy_llm = lite_llm, | |
| brief_llm = lite_llm, | |
| ) | |
| main_result = kickoff_with_retry(main_crew) | |
| else: | |
| raise | |
| raw_brief = str(main_result).strip() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PHASE 3 β Reflection Agent (pro_llm β lite fallback) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_reflection_crew(llm): | |
| reflection_agent = Agent( | |
| role="Executive Communications Editor", | |
| goal=( | |
| "Polish the executive brief so it is maximally clear, " | |
| "detailed, and actionable β ready to send to a C-suite audience." | |
| ), | |
| backstory=( | |
| "You are a senior communications editor for high-stakes documents. " | |
| "You remove redundancy, sharpen vague language, ensure every " | |
| "recommendation is specific, and guarantee logical flow." | |
| ), | |
| verbose=False, | |
| allow_delegation=False, | |
| llm=llm, | |
| ) | |
| reflection_task = Task( | |
| description=f""" | |
| Review and improve the following executive meeting brief. | |
| Editing checklist β apply EVERY item: | |
| 1. Remove any duplicate or repeated information across sections. | |
| 2. Sharpen vague language into specific, concrete statements. | |
| 3. Make every recommendation actionable (who does what, by when). | |
| 4. Fix any factual or logical inconsistencies between sections. | |
| 5. Ensure the brief flows logically: Summary β Context β Goals β | |
| Agenda β Q&A β Next Steps. | |
| 6. Cut filler words and padding, but keep important detail. | |
| 7. Keep all original markdown section headings intact. | |
| 8. Do NOT add new sections β only improve existing content. | |
| 9. Target length: 1100β1300 words (expand or compress as needed). | |
| ORIGINAL BRIEF TO IMPROVE: | |
| --- | |
| {raw_brief} | |
| --- | |
| OUTPUT: Return ONLY the improved markdown brief. | |
| No commentary, no preamble, no "Here is the improved version:" header. | |
| """, | |
| agent=reflection_agent, | |
| expected_output="An improved, polished executive brief in clean markdown format.", | |
| ) | |
| return Crew( | |
| agents=[reflection_agent], | |
| tasks=[reflection_task], | |
| verbose=False, | |
| max_rpm=4, | |
| max_execution_time=120, | |
| process=Process.sequential, | |
| ) | |
| reflection_result = kickoff_with_model_fallback(build_reflection_crew, high_quality=True) | |
| final_brief = str(reflection_result).strip() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PHASE 4 β Persist to Memory | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| summary_preview = ( | |
| final_brief[:350] | |
| .replace("\n", " ") | |
| .replace("#", "") | |
| .strip() | |
| ) | |
| saved = save_memory({ | |
| "timestamp": datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"), | |
| "company": company_name, | |
| "objective": meeting_objective, | |
| "summary": summary_preview, | |
| }) | |
| if not saved: | |
| print("[Memory] β οΈ Entry was NOT saved β check logs above.") | |
| except Exception as mem_exc: | |
| print(f"[Memory] β Phase 4 exception: {type(mem_exc).__name__}: {mem_exc}") | |
| elapsed = round(time.time() - start_time, 2) | |
| return jsonify({ | |
| "result": final_brief, | |
| "decision": decision_text, | |
| "memory_used": use_memory, | |
| "flags": decision_flags, | |
| "models": { | |
| "lite": "gemini/gemini-2.0-flash-lite", | |
| "pro": "gemini/gemini-2.5-flash", | |
| "search": "gemini/gemini-2.0-flash-lite", | |
| }, | |
| "elapsed_sec": elapsed, | |
| }), 200 | |
| except Exception as exc: | |
| msg = str(exc) | |
| if _is_rate_error(msg): | |
| return jsonify({ | |
| "error": ( | |
| "β οΈ Gemini API rate limit reached. " | |
| "Please wait 30β60 seconds and try again." | |
| ) | |
| }), 429 | |
| if "timed out" in msg.lower() or "timeout" in msg.lower(): | |
| return jsonify({ | |
| "error": ( | |
| "β±οΈ The agent pipeline took too long to respond. " | |
| "Try again β it usually succeeds on a second attempt." | |
| ) | |
| }), 503 | |
| return jsonify({"error": f"Agent error: {msg}"}), 500 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STANDALONE SEARCH AGENT β now with retry + elapsed time | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search_links(): | |
| """ | |
| Standalone link-search endpoint. | |
| Expects JSON: { "query": "..." } | |
| Returns: { query, links, model, elapsed_sec } | |
| """ | |
| data = request.get_json(force=True) | |
| query = data.get("query", "").strip() | |
| if not query: | |
| return jsonify({"error": "Missing or empty 'query' field."}), 400 | |
| start_time = time.time() | |
| try: | |
| search_agent = Agent( | |
| role="Link Discovery Specialist", | |
| goal=f"Find the most relevant and high-quality links for: {query}", | |
| backstory=( | |
| "You are an expert at navigating the web. You provide only the most " | |
| "authoritative and useful links, avoiding spam or low-quality sources. " | |
| "For each result you return the page title, a one-sentence description, " | |
| "and the full URL." | |
| ), | |
| tools=[search_tool], | |
| llm=search_llm, # uses lite_llm β preserves pro quota | |
| verbose=False, | |
| allow_delegation=False, | |
| ) | |
| search_task = Task( | |
| description=( | |
| f"Search the web and find the top 5β8 most relevant, authoritative " | |
| f"links related to: '{query}'.\n\n" | |
| f"For each result provide:\n" | |
| f"- **Title**: the page title\n" | |
| f"- **URL**: the full link\n" | |
| f"- **Summary**: one sentence describing what the page covers\n\n" | |
| f"Format the output as a clean markdown list." | |
| ), | |
| expected_output=( | |
| "A markdown list of 5β8 links, each with title, URL, and one-sentence summary." | |
| ), | |
| agent=search_agent, | |
| ) | |
| crew = Crew( | |
| agents=[search_agent], | |
| tasks=[search_task], | |
| verbose=False, | |
| max_rpm=9, | |
| max_execution_time=60, | |
| process=Process.sequential, | |
| ) | |
| result = kickoff_with_retry(crew, retries=2, base_wait=8) | |
| elapsed = round(time.time() - start_time, 2) | |
| return jsonify({ | |
| "query": query, | |
| "links": str(result).strip(), | |
| "model": "gemini/gemini-2.0-flash-lite", | |
| "elapsed_sec": elapsed, | |
| }), 200 | |
| except Exception as e: | |
| msg = str(e) | |
| elapsed = round(time.time() - start_time, 2) | |
| if _is_rate_error(msg): | |
| return jsonify({ | |
| "error": "β οΈ Rate limit reached. Please wait 30 seconds and try again.", | |
| "elapsed_sec": elapsed, | |
| }), 429 | |
| return jsonify({ | |
| "error": f"Search agent error: {msg}", | |
| "elapsed_sec": elapsed, | |
| }), 500 | |
| # ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| app.run(debug=True, use_reloader=False, host="0.0.0.0", port=7860) |