File size: 27,649 Bytes
e1af63d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
"""
agents.py (Day 2)

Contains the agentic logic for the Brain Dump Sanctuary.
This file provides the classes that app.py imports.

- QuestionAgent: Socratic question generation
- PerspectiveAgent: Multi-angle analysis
- SearchAgent: Web search + synthesis (MOCKED for Day 2)

This file uses simple, direct LLM calls for the Day 2 demo.
It is designed to be plug-and-play with app.py.

Future Work: These classes can be refactored into nodes
in a more complex LangGraph orchestration graph.
"""

import os
import json
import requests

# We'll use Google Gemini as the LLM
import google.generativeai as genai

from dotenv import load_dotenv

# Tavily for web search
from tavily import TavilyClient

# --- API Key Configuration ---
# Add your "GOOGLE_API_KEY" to Kaggle secrets or environment variables.
# Get your free key from: https://aistudio.google.com/app/apikey
try:
    load_dotenv()
    # Fallback for local dev or other environments
    API_KEY = os.getenv("GOOGLE_API_KEY")
except Exception:
    API_KEY = None

if not API_KEY:
    print("WARNING: GOOGLE_API_KEY not found. Please set it in your environment or Kaggle secrets.")
    print("Get your free API key from: https://aistudio.google.com/app/apikey")
    # Set a placeholder to avoid crashing, but calls will fail.
    API_KEY = "YOUR_API_KEY_HERE"
else:
    # Configure Gemini with the API key
    genai.configure(api_key=API_KEY)

# --- Tavily API Key Configuration ---
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
if not TAVILY_API_KEY:
    print("WARNING: TAVILY_API_KEY not found. SearchAgent will use mock data.")
    print("Get your API key from: https://app.tavily.com")
    TAVILY_API_KEY = None

# --- Perplexity API Key Configuration ---
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
if not PERPLEXITY_API_KEY:
    print("ℹ️ PERPLEXITY_API_KEY not found. Will use Tavily or mock data.")
    print("Get your API key from: https://www.perplexity.ai/")
    PERPLEXITY_API_KEY = None


# ============== 1. Socratic Question Agent ==============

class QuestionAgent:
    """
    Generates Socratic questions to expand on a vague idea.
    """
    def __init__(self, model="gemini-2.5-flash"):
        self.model = model
        self.system_prompt = """
You are a Socratic tutor. A user has a vague brain dump idea. 
Your goal is to generate 5 insightful, open-ended questions to help them 
explore this idea, discover its core, and understand their own curiosity.

- Do not answer the questions.
- Provide ONLY the list of questions.
- Return the questions as a JSON list of strings.

Example:
User Idea: "Why do we forget things we just read?"
Your Response:
[
    "What kind of material do you find you forget most often?",
    "What is your state of mind when you are reading?",
    "Are you trying to memorize facts, or understand a concept?",
    "What is the difference between remembering a fact and understanding an idea?",
    "How does this relate to the 'forgetting curve'?"
]
"""

    def generate_questions(self, idea: str) -> list[str]:
        """Generates Socratic questions for a given idea."""
        if API_KEY == "YOUR_API_KEY_HERE":
             return ["Error: GOOGLE_API_KEY is not set.", "Please add it to your environment or Kaggle secrets."]

        try:
            # Create Gemini model
            model = genai.GenerativeModel(
                model_name=self.model,
                generation_config={
                    "temperature": 0.7,
                    "response_mime_type": "application/json"
                }
            )
            
            # Combine system prompt and user input
            prompt = f"{self.system_prompt}\n\nUser Idea: \"{idea}\""
            
            # Generate response
            response = model.generate_content(prompt)
            questions_json_string = response.text
            questions = json.loads(questions_json_string)
            
            # The LLM might return a dict {"questions": [...]}, or just [...]
            if isinstance(questions, dict):
                # Try to find the list value
                for key, value in questions.items():
                    if isinstance(value, list):
                        return value
            elif isinstance(questions, list):
                return questions
            
            return ["Error: Could not parse questions from LLM response."]

        except Exception as e:
            print(f"Error in QuestionAgent: {e}")
            return [
                "An error occurred while generating questions.",
                "Is your API key set correctly?",
                f"Details: {e}"
            ]


# ============== 2. Multi-Perspective Agent ==============

class PerspectiveAgent:
    """
    Analyzes a controversial topic from multiple angles.
    Can optionally use web search results for context.
    """
    def __init__(self, model="gemini-2.5-flash", search_agent=None):
        self.model = model
        self.search_agent = search_agent
        self.system_prompt = """
You are a multi-perspective analyst. A user has a topic,
which may be controversial or nuanced.
Your goal is to provide three distinct, concise viewpoints:
1.  **Skeptical View:** The critical or cautious perspective.
2.  **Optimistic View:** The positive or enthusiastic perspective.
3.  **Nuanced View:** A balanced, synthetic view that includes trade-offs or a "third way".

You MUST return a JSON object with exactly three keys:
"skeptical", "optimistic", and "nuanced".
Each value should be a short paragraph (2-4 sentences).
"""

    def analyze(self, topic: str) -> dict[str, str]:
        """Analyzes a topic from multiple perspectives with optional web context."""
        if API_KEY == "YOUR_API_KEY_HERE":
            return {
                "skeptical": "Error: GOOGLE_API_KEY not set.",
                "optimistic": "Please add your API key to environment or Kaggle secrets.",
                "nuanced": "The agent cannot run without an API key."
            }

        try:
            # Get web context if search_agent is available
            web_context = ""
            sources = []
            if self.search_agent and self.search_agent.use_real_search:
                print(f"🌐 Fetching web context for multi-perspective analysis...")
                articles = self.search_agent.get_relevant_articles(topic)
                if articles:
                    web_context = "\n\nRelevant Web Context:\n"
                    for i, article in enumerate(articles, 1):
                        web_context += f"{i}. {article['title']}: {article['snippet']}\n"
                        sources.append(article)
            
            # Create Gemini model
            model = genai.GenerativeModel(
                model_name=self.model,
                generation_config={
                    "temperature": 0.7,
                    "response_mime_type": "application/json"
                }
            )
            
            # Combine system prompt and user input with optional web context
            prompt = f"{self.system_prompt}\n\nTopic: \"{topic}\"{web_context}"
            
            # Generate response
            response = model.generate_content(prompt)
            perspectives_json_string = response.text
            perspectives = json.loads(perspectives_json_string)
            
            # Ensure the keys are always present to prevent errors in app.py
            perspectives.setdefault('skeptical', 'No skeptical view generated.')
            perspectives.setdefault('optimistic', 'No optimistic view generated.')
            perspectives.setdefault('nuanced', 'No nuanced view generated.')
            
            # Add sources if available
            if sources:
                perspectives['sources'] = sources
            
            return perspectives
            
        except Exception as e:
            print(f"Error in PerspectiveAgent: {e}")
            return {
                "skeptical": f"An error occurred: {e}",
                "optimistic": "Please check your API key and model access.",
                "nuanced": "The PerspectiveAgent failed to run."
            }


# ============== 3. Web Search Agent ==============

class SearchAgent:
    """
    Real web search using Perplexity Sonar (preferred) or Tavily API + Gemini synthesis.
    Falls back to mock data if API keys are missing.
    """
    def __init__(self):
        self.use_perplexity = bool(PERPLEXITY_API_KEY)
        self.use_tavily = bool(TAVILY_API_KEY) and not self.use_perplexity
        self.use_real_search = self.use_perplexity or self.use_tavily
        
        if self.use_perplexity:
            self.perplexity_api_key = PERPLEXITY_API_KEY
            print("βœ… Initialized SearchAgent with Perplexity Sonar API")
        elif self.use_tavily:
            self.tavily = TavilyClient(api_key=TAVILY_API_KEY)
            self.llm = genai.GenerativeModel("gemini-2.5-flash")
            print("βœ… Initialized SearchAgent with Tavily API")
        else:
            print("⚠️ Initialized MOCK SearchAgent (no Perplexity or Tavily key)")
    
    def deep_dive(self, topic: str) -> dict:
        """
        Searches web using Perplexity Sonar or Tavily, then synthesizes.
        Falls back to mock if no API key.
        """
        if not self.use_real_search:
            return self._mock_search(topic)
        
        if self.use_perplexity:
            return self._perplexity_search(topic)
        else:
            return self._tavily_search(topic)
    
    def _perplexity_search(self, topic: str) -> dict:
        """Search using Perplexity Sonar API"""
        try:
            print(f"πŸ” Searching Perplexity Sonar for: {topic}")
            
            url = "https://api.perplexity.ai/chat/completions"
            headers = {
                "Authorization": f"Bearer {self.perplexity_api_key}",
                "Content-Type": "application/json"
            }
            
            payload = {
                "model": "sonar",
                "messages": [
                    {
                        "role": "system",
                        "content": "You are a helpful research assistant. Provide comprehensive, well-sourced answers."
                    },
                    {
                        "role": "user",
                        "content": f"Please provide a comprehensive summary about: {topic}"
                    }
                ],
                "max_tokens": 1000,
                "temperature": 0.7,
                "top_p": 0.9
            }
            
            response = requests.post(url, headers=headers, json=payload)
            response.raise_for_status()
            
            data = response.json()
            summary = data.get('choices', [{}])[0].get('message', {}).get('content', 'No summary generated')
            
            # Extract citations if available
            sources = []
            if 'citations' in data:
                sources = [{"title": f"Source {i+1}", "url": "#", "snippet": cite} 
                          for i, cite in enumerate(data['citations'][:5])]
            
            return {
                "summary": summary,
                "sources": sources
            }
            
        except Exception as e:
            print(f"❌ Perplexity search failed: {e}")
            return {
                "summary": f"Search failed: {str(e)}. Please check your Perplexity API key.",
                "sources": []
            }
    
    def _tavily_search(self, topic: str) -> dict:
        """
        Searches web using Tavily, then synthesizes with Gemini.
        """
        try:
            # Step 1: Search with Tavily
            print(f"πŸ” Searching Tavily for: {topic}")
            search_results = self.tavily.search(
                query=topic,
                max_results=5,
                search_depth="advanced"
            )
            
            # Step 2: Extract sources
            sources = []
            context = ""
            for result in search_results.get('results', []):
                sources.append({
                    'title': result.get('title', 'Untitled'),
                    'url': result.get('url', '#'),
                    'snippet': result.get('content', '')[:200] + "..."
                })
                context += f"\n\n{result.get('content', '')}"
            
            # Step 3: Synthesize with Gemini
            print(f"πŸ€– Synthesizing with Gemini...")
            synthesis_prompt = f"""You are a research synthesizer. Based on the following web search results about "{topic}", 
write a comprehensive 3-4 paragraph summary that:
1. Answers the core question
2. Highlights key findings and debates
3. Cites different viewpoints if applicable

Web Search Results:
{context[:4000]}

Provide ONLY the summary text, no preamble."""

            response = self.llm.generate_content(synthesis_prompt)
            summary = response.text
            
            return {
                "summary": summary,
                "sources": sources
            }
            
        except Exception as e:
            print(f"❌ Tavily search failed: {e}")
            return {
                "summary": f"Search failed: {str(e)}. Please check your Tavily API key.",
                "sources": []
            }
    
    def get_relevant_articles(self, topic: str, max_results: int = 3) -> list[dict]:
        """
        Gets top articles for a topic (used by PerspectiveAgent).
        Returns: [{'title': str, 'url': str, 'snippet': str}, ...]
        """
        if not self.use_real_search:
            return []
        
        try:
            if self.use_perplexity:
                print(f"πŸ“° Fetching articles with Perplexity for: {topic}")
                # Perplexity doesn't have a dedicated article fetch, use deep_dive results
                result = self._perplexity_search(topic)
                return result.get('sources', [])[:max_results]
            else:
                print(f"πŸ“° Fetching articles for: {topic}")
                search_results = self.tavily.search(
                    query=topic,
                    max_results=max_results,
                    search_depth="basic"
                )
                
                articles = []
                for result in search_results.get('results', []):
                    articles.append({
                        'title': result.get('title', 'Untitled'),
                        'url': result.get('url', '#'),
                        'snippet': result.get('content', '')[:300] + "..."
                    })
                
                return articles
            
        except Exception as e:
            print(f"❌ Article fetch failed: {e}")
            return []
    
    def _mock_search(self, topic: str) -> dict:
        """Fallback mock implementation"""
        print(f"πŸ” MOCK SEARCH: Deep dive for '{topic}' (using hardcoded data)")
        
        # Simulate different responses for different topics
        if "dream" in topic.lower():
            return {
                "summary": (
                    "Dreams are a complex neurological phenomenon, primarily occurring during "
                    "REM sleep. Research suggests they are crucial for memory consolidation, "
                    "emotional regulation, and problem-solving. The 'fading' "
                    "is attributed to the brain's different neurochemical state during sleep, "
                    "which is not optimized for encoding new memories."
                ),
                "sources": [
                    {"title": "The Science of Dreaming - Scientific American", "url": "https://www.scientificamerican.com/article/the-science-of-dreaming/"},
                    {"title": "Why We Dream - Psychology Today", "url": "https://www.psychologytoday.com/us/basics/dreaming"}
                ]
            }
        elif "llm" in topic.lower():
            return {
                "summary": (
                    "The debate on LLM 'understanding' is central to AI research. "
                    "One view holds they are 'stochastic parrots,' brilliantly matching "
                    "statistical patterns without true comprehension. "
                    "The opposing view suggests that at their scale, these models "
                    "develop emergent, internal world models, representing a new "
                    "form of understanding."
                ),
                "sources": [
                    {"title": "On the Dangers of Stochastic Parrots - FAccT '21", "url": "https://dl.acm.org/doi/10.1145/3442188.3445922"},
                    {"title": "Sparks of AGI: Early experiments with GPT-4", "url": "https://arxiv.org/abs/2303.12712"}
                ]
            }
        else:
            return {
                "summary": (
                    f"This is a mock summary about '{topic}'. This agent successfully simulated a "
                    "web search. In a real application, this text would be "
                    "dynamically generated by an LLM based on live search results "
                    "from a tool like Tavily or SerpAPI."
                ),
                "sources": [
                    {"title": "Mock Source 1 - Wikipedia", "url": "https://en.wikipedia.org/wiki/Main_Page"},
                    {"title": "Mock Source 2 - Example.com", "url": "https://example.com"}
                ]
            }

# --- How to refactor to LangGraph (Future Work) ---
"""
To implement your full "LangGraph" vision, you would:

1.  Define a State:
    class BrainDumpState(TypedDict):
        topic: str
        socratic_questions: list[str]
        perspectives: dict
        deep_dive: dict

2.  Create Nodes:
    - Each agent's method (e.g., `generate_questions`) becomes a node.
    - def question_node(state: BrainDumpState):
    -   questions = QuestionAgent().generate_questions(state['topic'])
    -   return {"socratic_questions": questions}
    - ... (similar nodes for perspective_node and search_node)

3.  Build the Graph:
    - workflow = StateGraph(BrainDumpState)
    - workflow.add_node("socratic", question_node)
    - workflow.add_node("perspectives", perspective_node)
    - workflow.add_node("search", search_node)
    - workflow.set_entry_point("socratic") # or a router
    - workflow.add_edge("socratic", "perspectives")
    - workflow.add_edge("perspectives", "search")
    - workflow.add_edge("search", END)

4.  Compile & Run:
    - app = workflow.compile()
    - # In app.py, you'd call:
    - # results = app.invoke({"topic": "Why do we dream?"})
    - # And then display results['socratic_questions'], etc.
    
This simple class-based approach is used for the Day 2 demo
as it directly matches your existing app.py implementation.
"""


# ============== 4. Feed Agent (using Perplexity Sonar) ==============

class FeedAgent:
    """
    Generates AI-powered summaries for feed cards using Perplexity Sonar.
    If Perplexity is not available, falls back to SearchAgent.
    Also fetches relevant images using Tavily image search.
    """
    def __init__(self, search_agent=None):
        self.search_agent = search_agent
        self.use_perplexity = bool(PERPLEXITY_API_KEY)
        self.perplexity_api_key = PERPLEXITY_API_KEY
        self.tavily_client = TavilyClient(api_key=TAVILY_API_KEY) if TAVILY_API_KEY else None
        
        if self.use_perplexity:
            print("βœ… Initialized FeedAgent with Perplexity Sonar")
        else:
            print("ℹ️ FeedAgent will use SearchAgent for summaries")
        
        if self.tavily_client:
            print("πŸ“Έ Initialized FeedAgent with Tavily Image Search")
        else:
            print("⚠️ Tavily API key not configured. Image search disabled.")
    
    def generate_summary(self, topic: str) -> dict:
        """
        Generate a comprehensive summary using Perplexity Sonar.
        Returns: {"summary": str, "sources": list}
        """
        if self.use_perplexity:
            return self._perplexity_summary(topic)
        elif self.search_agent:
            return self.search_agent.deep_dive(topic)
        else:
            return {
                "summary": "Unable to generate summary. Please configure Perplexity or Tavily API.",
                "sources": []
            }
    
    def _perplexity_summary(self, topic: str) -> dict:
        """Generate summary using Perplexity Sonar API"""
        try:
            print(f"🧠 Generating Sonar summary for: {topic}")
            
            url = "https://api.perplexity.ai/chat/completions"
            headers = {
                "Authorization": f"Bearer {self.perplexity_api_key}",
                "Content-Type": "application/json"
            }
            
            payload = {
                "model": "sonar",
                "messages": [
                    {
                        "role": "system",
                        "content": """You are a brilliant synthesizer of knowledge. Given a brain dump topic, 
provide a comprehensive yet concise summary that:
1. Explains the core concept clearly
2. Provides practical insights
3. Connects to broader contexts
4. Sparks further curiosity

Keep the tone engaging and thought-provoking."""
                    },
                    {
                        "role": "user",
                        "content": f"Provide a comprehensive summary about this brain dump topic: {topic}"
                    }
                ],
                "max_tokens": 1500,
                "temperature": 0.7,
                "top_p": 0.9,
                "search_domain_filter": ["perplexity.com"]
            }
            
            response = requests.post(url, headers=headers, json=payload)
            response.raise_for_status()
            
            data = response.json()
            summary = data.get('choices', [{}])[0].get('message', {}).get('content', 'No summary generated')
            
            # Extract citations if available
            sources = []
            if 'citations' in data:
                sources = [{"title": f"Source {i+1}", "url": "#", "snippet": cite} 
                          for i, cite in enumerate(data['citations'][:3])]
            
            return {
                "summary": summary,
                "sources": sources
            }
            
        except Exception as e:
            print(f"❌ Perplexity summary generation failed: {e}")
            # Fallback to SearchAgent if available
            if self.search_agent:
                print("Falling back to SearchAgent...")
                return self.search_agent.deep_dive(topic)
            
            return {
                "summary": f"Error generating summary: {str(e)}",
                "sources": []
            }
    
    def search_images(self, topic: str, max_results: int = 3) -> list[str]:
        """
        Search for relevant images using Tavily image search API.
        
        Args:
            topic: The topic to search for images
            max_results: Maximum number of images to return (default: 3)
            
        Returns:
            List of image URLs (up to max_results)
        """
        if not self.tavily_client:
            print("⚠️ Tavily API key not configured. Cannot search for images.")
            return []
        
        try:
            print(f"πŸ“Έ Searching for images related to: {topic}")
            
            # Use Tavily to search for images
            response = self.tavily_client.search(
                query=topic,
                max_results=max_results,
                include_images=True
            )
            
            # Extract image URLs from results
            image_urls = []
            
            # Try to extract images from the response
            if 'images' in response:
                for img in response['images'][:max_results]:
                    if isinstance(img, dict) and 'url' in img:
                        image_urls.append(img['url'])
                    elif isinstance(img, str):
                        image_urls.append(img)
            
            # If no images found in dedicated images field, try results
            if not image_urls and 'results' in response:
                for result in response['results'][:max_results]:
                    if isinstance(result, dict) and 'image' in result:
                        image_urls.append(result['image'])
            
            print(f"βœ“ Found {len(image_urls)} relevant images")
            return image_urls[:max_results]
            
        except Exception as e:
            print(f"❌ Image search failed: {e}")
            return []


# ============== 5. Brain Dump Generation Agent ==============

class GenerationAgent:
    """
    Generates creative braindumps based on a cluster's theme and entries.
    Takes a cluster name and list of entries, uses Gemini to synthesize
    a new braindump that fits the cluster and would be interesting to read.
    """
    def __init__(self, model="gemini-2.5-flash"):
        self.model = model
        self.system_prompt = """
You are a creative brainstorming agent. You've been given a cluster of related thoughts/brain dumps,
along with the cluster's theme.

Your task is to generate ONE new, engaging brain dump entry that:
1. Fits naturally with the theme and existing entries
2. Is inspired by the existing entries but presents a NEW angle or question
3. Is concise (1-2 sentences), matching the style of the existing entries
4. Introduces something the user might find interesting to explore
5. Does NOT simply repeat or combine existing entries

Generate ONLY the new brain dump text itself - no preamble, no explanation.
Just the thoughtful question or observation that belongs in this cluster.
"""

    def generate_braindump(self, cluster_name: str, entries: list[str]) -> str:
        """
        Generates a new braindump for a cluster.
        
        Args:
            cluster_name: The name/label of the cluster (e.g., "Dreams and Consciousness")
            entries: List of existing brain dump texts in this cluster
            
        Returns:
            Generated brain dump text as a string
        """
        if API_KEY == "YOUR_API_KEY_HERE":
            return "Error: GOOGLE_API_KEY is not set. Please add it to your environment."

        try:
            # Create Gemini model
            model = genai.GenerativeModel(
                model_name=self.model,
                generation_config={
                    "temperature": 0.8,  # Higher temperature for more creativity
                }
            )
            
            # Format entries for the prompt
            entries_str = "\n".join([f"- {entry}" for entry in entries])
            
            # Build the prompt
            prompt = f"""{self.system_prompt}

Cluster Theme: "{cluster_name}"

Existing entries in this cluster:
{entries_str}

Generate a new, creative brain dump entry that fits this cluster:"""
            
            # Generate response
            response = model.generate_content(prompt)
            generated_text = response.text.strip()
            
            # Clean up any extra quotes or markers
            if generated_text.startswith('"') and generated_text.endswith('"'):
                generated_text = generated_text[1:-1]
            
            return generated_text
            
        except Exception as e:
            print(f"Error in GenerationAgent: {e}")
            return f"Error generating braindump: {str(e)}"