File size: 24,417 Bytes
5e5c3f4
 
 
 
080a057
 
 
 
 
5e5c3f4
 
d31a5e4
 
 
 
d8a9b11
d31a5e4
 
4b91518
d31a5e4
5e5c3f4
d31a5e4
5e5c3f4
d31a5e4
 
 
5e5c3f4
d31a5e4
 
080a057
 
 
 
5e5c3f4
58c5059
5e5c3f4
58c5059
5e5c3f4
58c5059
5e5c3f4
 
 
58c5059
5e5c3f4
58c5059
d31a5e4
bcc08ec
 
 
 
 
 
 
 
 
5e5c3f4
 
 
 
 
 
 
d31a5e4
 
5e5c3f4
 
 
 
 
 
 
d31a5e4
080a057
 
 
 
 
5e703e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
080a057
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e5c3f4
 
 
 
 
5e703e5
080a057
5e703e5
 
 
 
080a057
5e5c3f4
 
 
 
 
 
 
d31a5e4
5e5c3f4
 
 
 
 
 
 
2288a3f
5e5c3f4
 
 
 
 
 
 
 
080a057
 
2288a3f
5e703e5
 
 
 
 
 
 
 
 
080a057
 
2288a3f
5e703e5
 
080a057
 
5e703e5
 
080a057
 
 
 
5e703e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
080a057
 
 
5e703e5
080a057
 
 
 
5e703e5
 
 
 
 
 
 
 
 
080a057
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5e703e5
080a057
 
 
 
5e703e5
 
5e5c3f4
080a057
d31a5e4
 
5e5c3f4
d31a5e4
 
 
5e5c3f4
 
 
 
 
 
 
 
 
d31a5e4
2288a3f
 
 
 
 
 
5e5c3f4
 
c8461ae
5e5c3f4
c8461ae
2288a3f
 
c8461ae
2288a3f
 
 
 
c8461ae
 
 
 
 
 
 
 
 
 
 
2288a3f
5e5c3f4
 
c8461ae
5e5c3f4
d31a5e4
 
5e5c3f4
0e61f1c
5e5c3f4
0e61f1c
080a057
 
5e5c3f4
 
 
 
 
 
d31a5e4
5e5c3f4
 
 
 
 
 
 
 
 
 
 
 
0e61f1c
5e5c3f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e6b038
5d32761
9e6b038
 
5e5c3f4
 
 
 
 
 
d31a5e4
5e5c3f4
 
 
 
 
e1d983e
5e5c3f4
0e61f1c
5e5c3f4
 
 
 
 
 
 
e1d983e
d31a5e4
5e5c3f4
 
 
 
 
 
 
 
eef8f3a
d31a5e4
0e61f1c
5e5c3f4
 
0e61f1c
 
5e5c3f4
 
 
 
 
 
d31a5e4
5e5c3f4
 
 
 
 
 
 
 
d31a5e4
5e5c3f4
 
 
 
 
 
 
 
 
 
2e052ba
0e61f1c
 
5e5c3f4
0e61f1c
5e5c3f4
d5de40b
5e5c3f4
0e61f1c
5e5c3f4
 
 
 
 
 
0e61f1c
 
5e5c3f4
 
 
 
 
 
 
 
080a057
 
5e5c3f4
 
 
 
 
 
 
3ccbf30
5e5c3f4
 
3ccbf30
5e5c3f4
080a057
 
5e5c3f4
 
 
 
3ccbf30
5e5c3f4
 
 
 
 
3ccbf30
5e5c3f4
 
 
080a057
 
 
 
 
 
 
5e5c3f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b31d8c
 
5e5c3f4
7b31d8c
 
 
 
 
9ec9c3b
7b31d8c
 
 
9ec9c3b
 
7b31d8c
 
 
 
 
9ec9c3b
7b31d8c
 
 
9ec9c3b
 
7b31d8c
 
 
 
 
9ec9c3b
7b31d8c
 
 
9ec9c3b
 
7b31d8c
 
 
 
9ec9c3b
7b31d8c
 
 
9ec9c3b
 
7b31d8c
 
d31a5e4
 
5e5c3f4
 
 
 
 
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
"""
Stateless Interview Chatbot Backend for HTML Frontend
All state management happens in the HTML/localStorage.
This backend only processes requests and returns responses.

NOW WITH CONTEXT MANAGEMENT:
- Automatically creates summaries when approaching token limits
- Keeps recent messages + summary of older ones
- Interviewer can continue indefinitely without hitting context limits
"""

import os
import gradio as gr
from datetime import datetime
from openai import OpenAI
from google import genai
from github import Github
from slugify import slugify
import github

# Configuration from environment variables
INTERVIEWER_BASE_URL = os.getenv("INTERVIEWER_BASE_URL", "http://localhost:8000/v1")
INTERVIEWER_API_KEY = os.getenv("INTERVIEWER_API_KEY", "")
INTERVIEWER_MODEL = os.getenv("INTERVIEWER_MODEL", "gpt-4")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "")
GITHUB_REPO = os.getenv("GITHUB_REPO", "")
GITHUB_BRANCH = os.getenv("GITHUB_BRANCH", "main")

# Context management settings
MAX_CONTEXT_TOKENS = 25000  # Conservative limit to leave room for response
KEEP_RECENT_MESSAGES = 20  # Keep last 10 exchanges (20 messages)

INITIAL_GREETING = """Hello! I'm here to help you share your project story with the community.

**Before we begin:**

I'll be using AI to conduct this interview and organize your responses into a well-structured article. The article will be submitted to a GitHub repository for review.

**To get started, please share:**
1. **Your project name** (or working title)
2. **A brief confirmation** that you're okay with AI helping to organize and write up this interview

Once I have that, we'll dive into your project's journey—from the initial spark, through challenges and decisions, to the real-world impact you've created!"""


def load_interview_instructions() -> str:
    """Load extra instructions for article generation from file."""
    instructions_path = os.path.join(os.path.dirname(__file__), "workflow_instructions.md")
    if os.path.exists(instructions_path):
        with open(instructions_path, "r", encoding="utf-8") as f:
            return f.read()
    return ""


def load_article_instructions() -> str:
    """Load extra instructions for article generation from file."""
    instructions_path = os.path.join(os.path.dirname(__file__), "article_instructions.md")
    if os.path.exists(instructions_path):
        with open(instructions_path, "r", encoding="utf-8") as f:
            return f.read()
    return ""


ARTICLE_GENERATION_PROMPT = """You are an expert editor who transforms interview transcripts into compelling case study articles.
Based on the following interview conversation, create a well-structured markdown article that tells the story of this project.
{extra_instructions}
**Interview Transcript:**
{transcript}
Generate the article in markdown format. Make it informative, inspiring, and practical for readers who might face similar challenges."""


def estimate_tokens(text: str) -> int:
    """Rough token estimate (4 chars ≈ 1 token)."""
    return len(text) // 4


def create_summary_with_context(previous_summary: str, new_messages: list[dict]) -> str:
    """
    Create an updated summary that incorporates both the previous summary and new messages.
    This avoids losing context when re-summarizing.
    """
    try:
        client = OpenAI(
            base_url=INTERVIEWER_BASE_URL,
            api_key=INTERVIEWER_API_KEY,
        )
        
        # Format new messages
        transcript = ""
        for msg in new_messages:
            role = "Interviewer" if msg["role"] == "assistant" else "Interviewee"
            transcript += f"{role}: {msg['content']}\n\n"
        
        summary_prompt = f"""You previously created this summary of an interview:

{previous_summary}

Now update it to include the following additional conversation that happened after:

{transcript}

Provide an updated comprehensive summary that:
- Preserves all key information from the previous summary
- Integrates the new conversation details
- Maintains all facts, decisions, challenges, solutions, and metrics
- Keeps it detailed but concise"""
        
        response = client.chat.completions.create(
            model=INTERVIEWER_MODEL,
            messages=[{"role": "user", "content": summary_prompt}],
            max_tokens=1500,
            temperature=0.3,
        )
        
        summary = response.choices[0].message.content
        print(f"✓ Updated summary ({estimate_tokens(summary)} tokens)")
        return summary
        
    except Exception as e:
        print(f"Summary update failed: {e}")
        # Fallback: append to previous summary
        return previous_summary + "\n\nAdditional context: Continued detailed discussion of the project."


def create_summary(history: list[dict]) -> str:
    """
    Create a summary of conversation history using the interviewer model.
    This preserves context while reducing token count.
    """
    try:
        client = OpenAI(
            base_url=INTERVIEWER_BASE_URL,
            api_key=INTERVIEWER_API_KEY,
        )
        
        # Format history for summary
        transcript = ""
        for msg in history:
            role = "Interviewer" if msg["role"] == "assistant" else "Interviewee"
            transcript += f"{role}: {msg['content']}\n\n"
        
        summary_prompt = f"""Summarize this interview conversation comprehensively. Preserve:
- Project name and key details
- All technical challenges and solutions discussed
- Important decisions and their rationale
- Metrics, outcomes, and impact mentioned
- Any specific technologies, tools, or frameworks
- Timeline and context information

Keep the summary detailed enough that the interviewer can continue naturally.

CONVERSATION:
{transcript}

Provide a comprehensive summary:"""
        
        response = client.chat.completions.create(
            model=INTERVIEWER_MODEL,
            messages=[{"role": "user", "content": summary_prompt}],
            max_tokens=1500,
            temperature=0.3,
        )
        
        summary = response.choices[0].message.content
        print(f"✓ Created summary ({estimate_tokens(summary)} tokens)")
        return summary
        
    except Exception as e:
        print(f"Summary creation failed: {e}")
        # Fallback: basic truncation summary
        return "Previous conversation covered project details and initial discussion."


def chat(history: list[dict], user_message: str) -> dict:
    """
    Process a chat message and return updated history.
    Stateless - all state comes from client.
    
    SMART CONTEXT MANAGEMENT:
    - Monitors token count
    - When approaching limit, creates ONE summary and stores it in history
    - Summary stored as special message: {"role": "system", "content": "...", "_type": "summary"}
    - On subsequent calls, reuses existing summary instead of re-summarizing
    - Periodically re-summarizes when recent messages grow too long
    
    Args:
        history: List of message dicts with 'role' and 'content'
        user_message: New message from user
        
    Returns:
        dict with 'history' and 'error' (if any)
    """
    try:
        if not INTERVIEWER_API_KEY or not INTERVIEWER_BASE_URL:
            return {
                "history": history,
                "error": "Interviewer API not configured"
            }
        
        # Build new history with user message
        new_history = history.copy() if history else []
        new_history.append({"role": "user", "content": user_message})
        
        # Get interviewer response
        client = OpenAI(
            base_url=INTERVIEWER_BASE_URL,
            api_key=INTERVIEWER_API_KEY,
        )
        
        # Load system instructions
        system_instructions = load_interview_instructions()
        
        # Check if we already have a summary in history
        existing_summary = None
        summary_index = -1
        for i, msg in enumerate(new_history):
            if msg.get("_type") == "summary":
                existing_summary = msg["content"]
                summary_index = i
                break
        
        # Estimate total tokens
        total_tokens = estimate_tokens(system_instructions)
        for msg in new_history:
            if msg.get("_type") != "summary":  # Don't count summary in total (it's in system context)
                total_tokens += estimate_tokens(msg["content"])
        
        print(f"Total tokens: ~{total_tokens} (limit: {MAX_CONTEXT_TOKENS})")
        if existing_summary:
            print(f"  Found existing summary at index {summary_index}")
        
        # Build messages for OpenAI API
        messages = [{"role": "system", "content": system_instructions}]
        
        # SMART CONTEXT MANAGEMENT
        if existing_summary:
            # We already have a summary - use it!
            # Get messages AFTER the summary point
            messages_after_summary = [m for i, m in enumerate(new_history) 
                                     if i > summary_index and m.get("_type") != "summary"]
            
            # Check if messages after summary are getting too long
            tokens_after_summary = sum(estimate_tokens(m["content"]) for m in messages_after_summary)
            
            if tokens_after_summary > MAX_CONTEXT_TOKENS * 0.6:  # 60% of limit
                # Time to re-summarize: combine old summary with some recent messages
                print(f"⚠ Re-summarizing: {tokens_after_summary} tokens after previous summary")
                
                # Get messages to summarize (everything except last KEEP_RECENT_MESSAGES)
                if len(messages_after_summary) > KEEP_RECENT_MESSAGES:
                    old_msgs_to_summarize = messages_after_summary[:-KEEP_RECENT_MESSAGES]
                    recent_messages = messages_after_summary[-KEEP_RECENT_MESSAGES:]
                    
                    # Create new summary that includes the old summary context
                    new_summary = create_summary_with_context(existing_summary, old_msgs_to_summarize)
                    
                    # Remove old summary from history
                    new_history = [m for i, m in enumerate(new_history) if i != summary_index]
                    
                    # Insert new summary at the beginning (after we've accumulated enough messages)
                    # Find where to insert (after initial greeting, before substantive conversation)
                    insert_pos = min(2, len(new_history))  # After first exchange typically
                    new_history.insert(insert_pos, {
                        "role": "system",
                        "content": new_summary,
                        "_type": "summary",
                        "_summarized_count": len(old_msgs_to_summarize)
                    })
                    
                    # Add summary to API messages
                    messages.append({
                        "role": "system",
                        "content": f"""CONVERSATION SUMMARY (updated):

{new_summary}

---

Continue the interview based on this context and recent messages below."""
                    })
                    
                    # Add recent messages
                    for msg in recent_messages:
                        messages.append({"role": msg["role"], "content": msg["content"]})
                    
                    print(f"✓ Re-summarized {len(old_msgs_to_summarize)} messages, keeping {len(recent_messages)} recent")
                else:
                    # Not enough messages yet to re-summarize, just use existing summary
                    messages.append({
                        "role": "system",
                        "content": f"""PREVIOUS CONVERSATION SUMMARY:

{existing_summary}

---

Continue the interview based on this context and recent messages below."""
                    })
                    
                    for msg in messages_after_summary:
                        messages.append({"role": msg["role"], "content": msg["content"]})
            else:
                # Reuse existing summary - no re-summarization needed!
                print(f"✓ Reusing existing summary ({tokens_after_summary} tokens after summary)")
                
                messages.append({
                    "role": "system",
                    "content": f"""PREVIOUS CONVERSATION SUMMARY:

{existing_summary}

---

Continue the interview based on this context and recent messages below."""
                })
                
                for msg in messages_after_summary:
                    messages.append({"role": msg["role"], "content": msg["content"]})
                    
        elif total_tokens > MAX_CONTEXT_TOKENS and len(new_history) > KEEP_RECENT_MESSAGES:
            # First time hitting the limit - create initial summary
            old_messages = new_history[:-KEEP_RECENT_MESSAGES]
            recent_messages = new_history[-KEEP_RECENT_MESSAGES:]
            
            print(f"⚠ First summarization! Summarizing {len(old_messages)} older messages...")
            
            # Create comprehensive summary
            summary = create_summary(old_messages)
            
            # Insert summary into history (so it persists on client side)
            insert_pos = min(2, len(new_history))
            new_history.insert(insert_pos, {
                "role": "system",
                "content": summary,
                "_type": "summary",
                "_summarized_count": len(old_messages)
            })
            
            # Add summary as additional system context
            messages.append({
                "role": "system",
                "content": f"""PREVIOUS CONVERSATION SUMMARY:

{summary}

---

You are now continuing the interview. The summary above covers earlier discussion. 
Continue naturally based on this context and the recent messages below."""
            })
            
            # Add recent messages for natural conversation flow
            for msg in recent_messages:
                messages.append({"role": msg["role"], "content": msg["content"]})
            
            new_token_estimate = (
                estimate_tokens(system_instructions) +
                estimate_tokens(summary) +
                sum(estimate_tokens(m["content"]) for m in recent_messages)
            )
            print(f"✓ After first summary: ~{new_token_estimate} tokens")
            
        else:
            # Add full conversation history (we're still within limits)
            for msg in new_history:
                if msg.get("_type") != "summary":  # Don't send summary as regular message
                    messages.append({"role": msg["role"], "content": msg["content"]})
        
        # Get response from interviewer
        response = client.chat.completions.create(
            model=INTERVIEWER_MODEL,
            messages=messages,
            max_tokens=1024,
            temperature=0.7,
        )
        
        assistant_message = response.choices[0].message.content
        new_history.append({"role": "assistant", "content": assistant_message})
        
        return {
            "history": new_history,
            "error": None
        }
        
    except Exception as e:
        error_msg = str(e)
        # Log full error for debugging
        print(f"Chat error: {error_msg}")
        
        # Provide user-friendly error message
        if "quota" in error_msg.lower() or "rate" in error_msg.lower() or "limit" in error_msg.lower() or "429" in error_msg:
            return {
                "history": history,
                "error": "AI quota exceeded. Please try again after 8 PM today or tomorrow morning."
            }
        elif "401" in error_msg or "authentication" in error_msg.lower() or "unauthorized" in error_msg.lower():
            return {
                "history": history,
                "error": "Authentication failed. API key may be invalid or expired."
            }
        elif "400" in error_msg:
            return {
                "history": history,
                "error": "AI service error. The AI may be temporarily unavailable. Please try again later."
            }
        elif "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
            return {
                "history": history,
                "error": "Request timed out. The AI service may be busy. Please try again in a moment."
            }
        elif "connection" in error_msg.lower() or "network" in error_msg.lower():
            return {
                "history": history,
                "error": "Connection failed. Please check your internet connection and try again."
            }
        return {
            "history": history,
            "error": f"AI temporarily unavailable. Please try again after 8 PM today or tomorrow. (Error: {error_msg[:100]})"
        }


def generate_article(history: list[dict]) -> dict:
    """
    Generate article from interview history.
    
    NOTE: Gemini has 2M token context, so no summarization needed here.
    
    Args:
        history: Complete conversation history
        
    Returns:
        dict with 'article' and 'error' (if any)
    """
    try:
        if not GEMINI_API_KEY:
            return {
                "article": None,
                "error": "Gemini API key not configured"
            }
        
        if len(history) < 4:
            return {
                "article": None,
                "error": "Please have a longer interview before generating article"
            }
        
        client = genai.Client(api_key=GEMINI_API_KEY)
        
        # Format transcript
        transcript = ""
        for msg in history:
            role = "Interviewer" if msg["role"] == "assistant" else "Interviewee"
            transcript += f"**{role}:** {msg['content']}\n\n"
        
        # Load extra instructions
        extra_instructions = load_article_instructions()
        
        prompt = ARTICLE_GENERATION_PROMPT.format(
            transcript=transcript,
            extra_instructions=extra_instructions if extra_instructions else "Use best practices for case study writing."
        )
        
        response = client.models.generate_content(
            model="gemini-pro-latest",
            contents=prompt
        )
        
        return {
            "article": response.text,
            "error": None
        }
        
    except Exception as e:
        return {
            "article": None,
            "error": f"Failed to generate article: {str(e)}"
        }


def submit_article(article_content: str) -> dict:
    """
    Submit article to GitHub.
    
    Args:
        article_content: The markdown article
        
    Returns:
        dict with 'status', 'url', 'filename', and 'error'
    """
    try:
        if not GITHUB_TOKEN or not GITHUB_REPO:
            return {
                "status": "error",
                "error": "GitHub not configured",
                "url": None,
                "filename": None
            }
        
        g = Github(auth=github.Auth.Token(GITHUB_TOKEN))
        repo = g.get_repo(GITHUB_REPO)
        
        # Generate filename from article title
        date_str = datetime.now().strftime("%Y-%m-%d")
        lines = article_content.split('\n')
        title_line = next((l for l in lines if l.startswith('# ')), None)
        
        if title_line:
            slug = slugify(title_line[2:].strip()[:50])
        else:
            slug = f"interview-{datetime.now().strftime('%H%M%S')}"
        
        filename = f"_draft/{date_str}-{slug}.md"
        
        # Add front matter
        front_matter = f"""---
date: {date_str}
status: draft
source: interview-chatbot
---
"""
        full_content = front_matter + article_content
        
        # Create file in GitHub
        repo.create_file(
            path=filename,
            message=f"Add draft article: {slug}",
            content=full_content,
            branch=GITHUB_BRANCH
        )
        
        github_url = f"https://github.com/{GITHUB_REPO}/blob/{GITHUB_BRANCH}/{filename}"
        
        return {
            "status": "success",
            "url": github_url,
            "filename": filename,
            "error": None
        }
        
    except Exception as e:
        return {
            "status": "error",
            "error": str(e),
            "url": None,
            "filename": None
        }


def get_initial_greeting() -> str:
    """Return the initial greeting message."""
    return INITIAL_GREETING


# Build Gradio interface - STATELESS
with gr.Blocks(title="Interview Chatbot API") as demo:
    gr.Markdown("# Interview Chatbot Backend API")
    gr.Markdown("This is a stateless backend with **automatic context management**.")
    gr.Markdown("Long interviews are automatically summarized to stay within token limits.")
    
    with gr.Tab("API Documentation"):
        gr.Markdown("""
        ## Available Endpoints
        
        ### POST /api/chat
        **Input:** `[history, user_message]`
        - `history`: Array of message objects `[{{role, content}}, ...]`
        - `user_message`: String
        
        **Output:** `{{history, error}}`
        
        **Context Management:** Automatically creates summaries when approaching token limits
        
        ### POST /api/generate_article
        **Input:** `[history]`
        - `history`: Array of message objects
        
        **Output:** `{{article, error}}`
        
        ### POST /api/submit_article  
        **Input:** `[article_content]`
        - `article_content`: Markdown string
        
        **Output:** `{{status, url, filename, error}}`
        
        ### GET /api/get_initial_greeting
        **Output:** Initial greeting string
        
        ## Settings
        
        - **Max Context:** {MAX_CONTEXT_TOKENS:,} tokens
        - **Recent Messages Kept:** {KEEP_RECENT_MESSAGES} (last exchanges preserved)
        - **Article Generator:** Gemini 2.5 Pro (2M token context - no limit)
                """.format(MAX_CONTEXT_TOKENS=MAX_CONTEXT_TOKENS, KEEP_RECENT_MESSAGES=KEEP_RECENT_MESSAGES))
    
    with gr.Tab("Test Interface"):
        with gr.Row():
            test_history = gr.JSON(label="History", value=[])
            test_message = gr.Textbox(label="User Message", placeholder="Type a message...")
        
        test_chat_btn = gr.Button("Test Chat")
        test_output = gr.JSON(label="Response")
        
        test_chat_btn.click(
            fn=chat,
            inputs=[test_history, test_message],
            outputs=[test_output]
        )
    
    # Hidden API endpoints (for the HTML client)
    # These don't show in the UI but are accessible via the API
    
    with gr.Row(visible=False):
        # Chat endpoint
        chat_history_input = gr.JSON()
        chat_message_input = gr.Textbox()
        chat_output = gr.JSON()
        chat_btn = gr.Button("Chat")
        chat_btn.click(
            fn=chat,
            inputs=[chat_history_input, chat_message_input],
            outputs=[chat_output],
            api_name="chat"
        )
        
        # Generate article endpoint
        gen_history_input = gr.JSON()
        gen_output = gr.JSON()
        gen_btn = gr.Button("Generate")
        gen_btn.click(
            fn=generate_article,
            inputs=[gen_history_input],
            outputs=[gen_output],
            api_name="generate_article"
        )
        
        # Submit article endpoint
        submit_input = gr.Textbox()
        submit_output = gr.JSON()
        submit_btn_api = gr.Button("Submit")
        submit_btn_api.click(
            fn=submit_article,
            inputs=[submit_input],
            outputs=[submit_output],
            api_name="submit_article"
        )
        
        # Get initial greeting endpoint
        greeting_output = gr.Textbox()
        greeting_btn = gr.Button("Greeting")
        greeting_btn.click(
            fn=get_initial_greeting,
            inputs=[],
            outputs=[greeting_output],
            api_name="get_initial_greeting"
        )
        

if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
    )