File size: 36,080 Bytes
7f2cf0c
 
 
 
 
61b3ce7
7f2cf0c
 
 
 
5e6b2a8
7f2cf0c
0d81ba5
 
 
 
 
 
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6130b74
27dfca2
 
 
 
 
0d81ba5
61b3ce7
 
0d81ba5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61b3ce7
0d81ba5
 
 
 
 
 
 
61b3ce7
0d81ba5
 
7f2cf0c
27dfca2
 
 
 
 
61b3ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12d0925
 
 
 
5e6b2a8
 
12d0925
 
 
 
 
5e6b2a8
 
 
 
 
 
 
 
 
12d0925
 
 
 
 
 
 
 
 
7b08cfa
7f2cf0c
f5b1e50
12d0925
 
 
 
 
 
7b08cfa
 
 
 
12d0925
 
0d70fcf
12d0925
0d70fcf
12d0925
 
 
5e6b2a8
 
 
 
 
 
 
12d0925
 
 
 
7f2cf0c
 
7b08cfa
7f2cf0c
7b08cfa
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
 
 
 
bf33606
7f2cf0c
 
61b3ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
 
 
 
 
12d0925
7f2cf0c
 
 
12d0925
7f2cf0c
12d0925
7f2cf0c
 
 
 
 
61b3ce7
 
 
 
 
 
 
 
7f2cf0c
 
0d70fcf
7f2cf0c
0d70fcf
12d0925
7f2cf0c
12d0925
 
 
7f2cf0c
 
 
 
 
 
 
12d0925
7f2cf0c
12d0925
 
61b3ce7
 
7f2cf0c
 
 
27dfca2
 
 
 
12d0925
7f2cf0c
0d70fcf
7f2cf0c
0d70fcf
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
 
12d0925
7f2cf0c
 
 
 
 
 
 
61b3ce7
7f2cf0c
 
 
 
 
 
27dfca2
 
7f2cf0c
 
 
0d70fcf
7f2cf0c
0d70fcf
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
 
0d70fcf
7f2cf0c
0d70fcf
12d0925
7f2cf0c
12d0925
7f2cf0c
12d0925
7f2cf0c
 
 
12d0925
61b3ce7
27dfca2
12d0925
7f2cf0c
 
 
27dfca2
 
7f2cf0c
 
 
 
 
12d0925
7f2cf0c
 
 
 
 
 
 
 
 
 
 
12d0925
7f2cf0c
 
 
 
 
 
 
 
 
 
 
12d0925
 
7f2cf0c
27dfca2
 
 
7f2cf0c
27dfca2
 
 
7f2cf0c
 
0d81ba5
 
 
 
 
 
7f2cf0c
0d81ba5
 
 
 
 
7f2cf0c
 
 
 
61b3ce7
 
 
7f2cf0c
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dae45e7
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d81ba5
 
27dfca2
0d81ba5
 
7f2cf0c
0d81ba5
 
27dfca2
0d81ba5
 
7f2cf0c
 
 
 
0d81ba5
 
 
 
 
 
 
 
27dfca2
0d81ba5
 
 
 
 
 
 
 
 
 
 
 
27dfca2
0d81ba5
 
 
 
 
 
 
 
 
 
 
 
27dfca2
0d81ba5
 
 
 
 
 
 
 
 
 
 
 
27dfca2
0d81ba5
 
 
 
 
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d81ba5
 
 
 
 
27dfca2
0d81ba5
 
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
 
 
 
 
61b3ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
 
 
 
 
 
 
 
 
 
 
 
 
 
0d81ba5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f2cf0c
 
61b3ce7
 
 
 
7f2cf0c
 
 
27dfca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
import hmac
import hashlib
import json
import os
import requests
import threading
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from .models import ChatSession, PendingMessage, UserProfile
from urllib.parse import parse_qs
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List

SUPPORTED_LANGUAGES = [
    {'name': 'Bulgarian', 'code': 'bg', 'small': True, 'regional': False, 'flag': '🇧🇬'},
    {'name': 'Croatian', 'code': 'hr', 'small': True, 'regional': False, 'flag': '🇭🇷'},
    {'name': 'Czech', 'code': 'cs', 'small': True, 'regional': False, 'flag': '🇨🇿'},
    {'name': 'Danish', 'code': 'da', 'small': True, 'regional': False, 'flag': '🇩🇰'},
    {'name': 'Dutch', 'code': 'nl', 'small': False, 'regional': False, 'flag': '🇳🇱'},
    {'name': 'English', 'code': 'en', 'small': False, 'regional': False, 'flag': '🇬🇧'},
    {'name': 'Estonian', 'code': 'et', 'small': True, 'regional': False, 'flag': '🇪🇪'},
    {'name': 'Finnish', 'code': 'fi', 'small': True, 'regional': False, 'flag': '🇫🇮'},
    {'name': 'French', 'code': 'fr', 'small': False, 'regional': False, 'flag': '🇫🇷'},
    {'name': 'German', 'code': 'de', 'small': False, 'regional': False, 'flag': '🇩🇪'},
    {'name': 'Greek', 'code': 'el', 'small': True, 'regional': False, 'flag': '🇬🇷'},
    {'name': 'Hungarian', 'code': 'hu', 'small': True, 'regional': False, 'flag': '🇭🇺'},
    {'name': 'Irish', 'code': 'ga', 'small': True, 'regional': False, 'flag': '🇮🇪'},
    {'name': 'Italian', 'code': 'it', 'small': False, 'regional': False, 'flag': '🇮🇹'},
    {'name': 'Latvian', 'code': 'lv', 'small': True, 'regional': False, 'flag': '🇱🇻'},
    {'name': 'Lithuanian', 'code': 'lt', 'small': True, 'regional': False, 'flag': '🇱🇹'},
    {'name': 'Maltese', 'code': 'mt', 'small': True, 'regional': False, 'flag': '🇲🇹'},
    {'name': 'Polish', 'code': 'pl', 'small': False, 'regional': False, 'flag': '🇵🇱'},
    {'name': 'Portuguese', 'code': 'pt', 'small': False, 'regional': False, 'flag': '🇵🇹'},
    {'name': 'Romanian', 'code': 'ro', 'small': False, 'regional': False, 'flag': '🇷🇴'},
    {'name': 'Slovak', 'code': 'sk', 'small': True, 'regional': False, 'flag': '🇸🇰'},
    {'name': 'Slovenian', 'code': 'sl', 'small': True, 'regional': False, 'flag': '🇸🇮'},
    {'name': 'Spanish', 'code': 'es', 'small': False, 'regional': False, 'flag': '🇪🇸'},
    {'name': 'Swedish', 'code': 'sv', 'small': True, 'regional': False, 'flag': '🇸🇪'},
    {'name': 'Breton', 'code': 'br', 'small': True, 'regional': True, 'flag': '🏴'},
    {'name': 'Catalan', 'code': 'ca', 'small': True, 'regional': True, 'flag': '🇦🇩'},
    {'name': 'Welsh', 'code': 'cy', 'small': True, 'regional': True, 'flag': '🏴󠁧󠁢󠁷󠁬󠁳󠁿'},
    {'name': 'Scottish Gaelic', 'code': 'gd', 'small': True, 'regional': True, 'flag': '🏴󠁧󠁢󠁳󠁣󠁴󠁿'},
    {'name': 'Ukrainian', 'code': 'uk', 'small': False, 'regional': False, 'flag': '🇺🇦'}
]

class WordAnalysis(BaseModel):
    form: str = Field(description="The word as it appears in the text")
    grammar_comments: str = Field(description="Grammatical comments about the word")
    lemma: str = Field(description="The dictionary form of the word")
    translation: str = Field(description="The English translation of the word")

class WordAnalysisList(BaseModel):
    words: List[WordAnalysis]

class StylisticSentence(BaseModel):
    sent_id: int = Field(description="The unique ID of the sentence")
    meaning: str = Field(description="The general meaning of the sentence in English")
    explanation: str = Field(description="An explanation of stylistic choices, colloquialisms, slang, or dialect features.")

class StylisticAnalysis(BaseModel):
    sentences: List[StylisticSentence]

class GrammarSpellingCheck(BaseModel):
    word: str = Field(description="The misspelled word")
    correction: str = Field(description="The corrected form")

class GrammarSentence(BaseModel):
    original: str = Field(description="The original sentence")
    rewritten: str = Field(description="The rewritten sentence with better grammar/style")
    explanation: str = Field(description="Explanation for the changes made. Leave empty if no changes were needed.")

class GrammarAnalysis(BaseModel):
    is_translation: bool = Field(description="True if the input was NOT in the target language and had to be translated")
    translations: List[str] = Field(description="Sentence-by-sentence translations (only if is_translation is True)", default_factory=list)
    spelling_checks: List[GrammarSpellingCheck] = Field(description="List of spelling errors found", default_factory=list)
    sentences: List[GrammarSentence] = Field(description="Sentence-by-sentence rewriting and explanation", default_factory=list)

class NativeSuggestion(BaseModel):
    style: str = Field(description="Description of the style (e.g., Informal, Formal, Idiomatic)")
    text: str = Field(description="The rewritten text in this style")

class NativeRewriteAnalysis(BaseModel):
    suggestions: List[NativeSuggestion]

# Inizializzazione AI Con LangChain
llm_chat = HuggingFaceEndpoint(
    repo_id="meta-llama/Llama-3.1-8B-Instruct",
    huggingfacehub_api_token=settings.HF_TOKEN,
    task="text-generation",
    max_new_tokens=1500,
)
chat_model_main = ChatHuggingFace(llm=llm_chat)

llm_util = HuggingFaceEndpoint(
    repo_id="Qwen/Qwen2.5-7B-Instruct",
    huggingfacehub_api_token=settings.HF_TOKEN,
    task="text-generation",
    max_new_tokens=3000,
)
chat_model_util = ChatHuggingFace(llm=llm_util)

# Modelli per lingue meno diffuse: EuroLLM per EU ufficiali, Aya per regionali
EUROLLM_MODEL = "utter-project/EuroLLM-22B-Instruct-2512"
AYA_MODEL = "CohereLabs/aya-expanse-32b"
HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions"

# Dizionario globale per gestire i timer dei messaggi (per chat_id)
active_timers = {}

def fire_delayed_reply(chat_id):
    """Funzione chiamata dal timer per processare la risposta AI"""
    from .models import ChatSession
    try:
        chat = ChatSession.objects.get(id=chat_id)
        process_chat_reply(chat)
    except Exception as e:
        print(f"Errore timer: {e}")
    finally:
        if chat_id in active_timers:
            del active_timers[chat_id]

def validate_init_data(init_data):
    """
    Valida i dati di inizializzazione di Telegram.
    """
    if not init_data:
        return None
    
    parsed_data = parse_qs(init_data)
    data_check_string = "\n".join(f"{k}={v[0]}" for k, v in sorted(parsed_data.items()) if k != "hash")
    
    secret_key = hmac.new(b"WebAppData", settings.TELEGRAM_TOKEN.encode(), hashlib.sha256).digest()
    calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
    
    if calculated_hash == parsed_data.get("hash", [""])[0]:
        user_data = json.loads(parsed_data.get("user", ["{}"])[0])
        return user_data
    return None

from django.contrib.auth import login, authenticate
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required

from django.utils import timezone

def get_or_create_tg_user(user_data):
    """
    Sincronizza l'utente Telegram con il DB Django.
    """
    username = f"tg_{user_data['id']}"
    user = User.objects.filter(username=username).first()
    if not user:
        user = User.objects.create(
            username=username,
            first_name=user_data.get('first_name', ''),
            last_name=user_data.get('last_name', ''),
            last_login=timezone.now(),
            date_joined=timezone.now()
        )
    
    # Ensure profile exists
    UserProfile.objects.get_or_create(user=user)
    return user

def ensure_user_session(request):
    if request.user.is_authenticated:
        return request.user
    
    # Try to get data from URL or Headers
    init_data = request.GET.get('initData') or request.headers.get('X-Telegram-Init-Data')
    
    if init_data:
        user_data = validate_init_data(init_data)
        if user_data:
            user = get_or_create_tg_user(user_data)
            login(request, user)
            return user
        else:
            # DEBUG: If validation fails, it's usually a wrong TELEGRAM_TOKEN
            print("TELEGRAM VALIDATION FAILED") 
            
    return None

@csrf_exempt
def login_page(request):

    if request.method == 'POST':
        username = request.POST.get('username')
        if username:
            user = User.objects.filter(username=username).first()
            if not user:
                user = User.objects.create(
                    username=username,
                    last_login=timezone.now(),
                    date_joined=timezone.now()
                )
            UserProfile.objects.get_or_create(user=user)
            login(request, user)
            return redirect('index')
    return render(request, 'web/login.html')

def index(request):
    user = ensure_user_session(request)
    
    if not user:
        # If we are NOT logged in, we still show the index page, 
        # but with an empty chat list. The JS will then try to log in.
        return render(request, 'web/index.html', {
            'chats': [], 
            'user_name': 'Guest'
        })
        
    chats = user.chats.all().order_by('-created_at')
    return render(request, 'web/index.html', {
        'chats': chats, 
        'user_name': user.first_name
    })
def chat_detail(request, chat_id):
    user = ensure_user_session(request)
    if not user:
        return redirect('login_page')
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)
    messages = chat.messages.all().order_by('created_at')

    
    return render(request, 'web/chat.html', {
        'chat': chat,
        'messages': messages,
        'user_name': user.first_name
    })

@csrf_exempt
def check_messages(request, chat_id):
    user = ensure_user_session(request)
    if not user:
        return JsonResponse({'error': 'Unauthorized'}, status=401)
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)
    messages = chat.messages.all().order_by('created_at')
    
    msgr_list = []
    for m in messages:
        msgr_list.append({
            'id': m.id,
            'content': m.content,
            'is_user': m.is_user,
            'created_at': m.created_at.isoformat()
        })
    
    return JsonResponse({'messages': msgr_list})

@csrf_exempt
def send_message(request, chat_id):
    if request.method != 'POST':
        return JsonResponse({'error': 'POST only'}, status=405)
    
    user = ensure_user_session(request)
    if not user:
        return JsonResponse({'error': 'Unauthorized'}, status=401)
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)
    data = json.loads(request.body)

    content = data.get('content')
    
    if content:
        msg = PendingMessage.objects.create(chat=chat, content=content, is_user=True)
        
        # Gestione Timer per risposte differite
        if chat.id not in active_timers:
            timer_duration = chat.reply_timer if chat.reply_timer > 0 else 0.5
            timer = threading.Timer(timer_duration, fire_delayed_reply, args=[chat.id])
            active_timers[chat.id] = timer
            timer.start()
            
        return JsonResponse({'status': 'ok', 'delayed': True})
    return JsonResponse({'error': 'missing content'}, status=400)

@csrf_exempt
def create_new_chat(request):

    user = ensure_user_session(request)
    if not user:
        return redirect('login_page')
    
    profile, _ = UserProfile.objects.get_or_create(user=user)
    
    if request.method == 'POST':
        name = request.POST.get('name')
        bio = request.POST.get('bio')
        lang = request.POST.get('lang')
        
        chat = ChatSession.objects.create(
            user=user,
            chat_name=name,
            character_bio=bio if bio else profile.global_bio,
            language=lang if lang else profile.global_language,
            mcp_tools=profile.global_mcp_tools,
            reply_timer=int(request.POST.get('reply_timer', profile.global_timer))
        )
        return redirect('chat_detail', chat_id=chat.id)
    
    return render(request, 'web/new_chat.html', {
        'profile': profile,
        'supported_languages': SUPPORTED_LANGUAGES
    })


@csrf_exempt
def chat_settings(request, chat_id):

    user = ensure_user_session(request)
    if not user:
        return redirect('login_page')
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)
    
    if request.method == 'POST':

        chat.chat_name = request.POST.get('name')
        chat.character_bio = request.POST.get('bio')
        chat.language = request.POST.get('lang')
        mcp_list = request.POST.getlist('mcp_tools')
        
        # Filter empty strings & save
        chat.mcp_tools = [url.strip() for url in mcp_list if url.strip()]
        chat.reply_timer = int(request.POST.get('reply_timer', chat.reply_timer))
        chat.save()
        
        return redirect('chat_detail', chat_id=chat.id)

    
    return render(request, 'web/chat_settings.html', {
        'chat': chat,
        'supported_languages': SUPPORTED_LANGUAGES
    })


@csrf_exempt
def delete_chat(request, chat_id):

    user = ensure_user_session(request)
    if not user:
        return redirect('login_page')
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)
    chat.delete()

    return redirect('index')

@csrf_exempt
def global_settings(request):

    user = ensure_user_session(request)
    if not user:
        return redirect('login_page')
    
    profile, _ = UserProfile.objects.get_or_create(user=user)
    
    if request.method == 'POST':
        mcp_list = request.POST.getlist('mcp_tools')
        profile.global_mcp_tools = [url.strip() for url in mcp_list if url.strip()]
        profile.global_timer = int(request.POST.get('global_timer', profile.global_timer))
        profile.global_language = request.POST.get('global_language', profile.global_language)
        profile.save()
        return redirect('index')
    
    return render(request, 'web/settings.html', {
        'profile': profile,
        'supported_languages': SUPPORTED_LANGUAGES
    })





@csrf_exempt
def telegram_webhook(request):
    # Logica per gestire i messaggi da Telegram bot
    # Se il bot è ancora attivo, può inviare i dati qui
    return HttpResponse("OK")

@csrf_exempt
def ai_action(request, chat_id):
    if request.method != 'POST':
        return JsonResponse({'error': 'POST only'}, status=405)
    
    user = ensure_user_session(request)
    if not user:
        return JsonResponse({'error': 'Unauthorized'}, status=401)
    
    data = json.loads(request.body)
    action = data.get('action')
    text_to_analyze = data.get('text')
    user_question = data.get('question', '')
    
    if not action or not text_to_analyze:
        return JsonResponse({'error': 'Missing parameters'}, status=400)
    
    chat = get_object_or_404(ChatSession, id=chat_id, user=user)

    target_lang = chat.language
    lang_info = next((l for l in SUPPORTED_LANGUAGES if l['name'] == target_lang), None)
    # Per le lingue "piccole" usiamo MADLAD in cascata o per traduzioni dirette
    is_small_lang = lang_info['small'] if lang_info else False
    
    # Inizialmente usiamo sempre il modello base per la struttura
    current_util_model = chat_model_util

    # Prompt logic based on bot copy.py
    if action == "grammar_check":
        prompt = (
            f"Analyze the following text for grammatical errors in {target_lang}. "
            f"First, check if the text is predominantly in {target_lang}. If NOT, set is_translation to true and provide sentence-by-sentence translations. "
            f"If it IS in {target_lang}, set is_translation to false, identify spelling errors, and provide a sentence-by-sentence rewrite with explanations for any changes. "
            f"\n\nText: {text_to_analyze}"
        )
    elif action == "native_rewrite":
        prompt = (
            f"Rewrite the following text to sound more native and natural in {target_lang}. "
            f"Provide multiple suggestions, each with a brief description of the native style (e.g., Informal, Formal, Idiomatic). "
            f"\n\nText: {text_to_analyze}"
        )
    elif action == "open_question":
        prompt = f"The user has a specific question about this text in {target_lang}:\n'{user_question}'\n\nPlease answer the question based on this context:\nText: {text_to_analyze}"

    elif action == "translate":
        prompt = f"Translate the following text into English. Provide ONLY the translation without any introduction.\n\nText: {text_to_analyze}."
    elif action == "translate_to_target":
        prompt = f"Translate the following English text into {target_lang}. Provide ONLY the translation without any introduction.\n\nText: {text_to_analyze}."
    elif action == "word_to_word":
        if target_lang:
            lang_code = lang_info['code'] if lang_info else "en"
            api_url = f"https://randusertry-stanzalazymodels.hf.space/{lang_code}/analyze"
            try:
                # Chiamata API esterna (Stanza as a Service) per analisi morfo-sintattica
                resp = requests.post(api_url, json={"text": text_to_analyze}, timeout=60)
                if resp.status_code == 200:
                    data = resp.json()
                    word_objects = []
                    lemmas_to_translate = []
                    
                    # Gestione flessibile della risposta (flat list vs nested sentences)
                    if isinstance(data, list):
                        # Caso 1: Lista piatta di parole (come visto nell'output PowerShell dell'utente)
                        if len(data) > 0 and isinstance(data[0], dict) and ('text' in data[0] or 'lemma' in data[0]):
                            words_to_process = data
                        else:
                            # Caso 2: Lista di frasi (che a loro volta sono liste di parole)
                            words_to_process = []
                            for sent in data:
                                if isinstance(sent, list):
                                    words_to_process.extend(sent)
                                elif isinstance(sent, dict) and "words" in sent:
                                    words_to_process.extend(sent["words"])
                    else:
                        # Caso 3: Dizionario con chiave "sentences"
                        words_to_process = []
                        for sent in data.get("sentences", []):
                            words_to_process.extend(sent if isinstance(sent, list) else sent.get("words", []))
                    
                    for word in words_to_process:
                        if not isinstance(word, dict): continue
                        # L'API usa 'pos' e 'morph' invece di 'upos' e 'feats'
                        upos = word.get("pos") or word.get("upos", "")
                        morph = word.get("morph") or word.get("feats", "")
                        
                        if upos == 'PUNCT': continue
                        
                        word_objects.append({
                            "form": word.get("text"),
                            "grammar_comments": f"{upos} {morph}".strip(),
                            "lemma": word.get("lemma"),
                            "translation": ""
                        })
                        if word.get("lemma"):
                            lemmas_to_translate.append(word.get("lemma"))
                    
                    # Route to EuroLLM (official EU) or Aya (regional) for translation
                    if lemmas_to_translate:
                        lemmas_to_translate = [l for l in lemmas_to_translate if any(c.isalpha() for c in l)]
                        lang_meta = next((l for l in SUPPORTED_LANGUAGES if l['name'] == chat.language), None)
                        is_regional = lang_meta.get('regional', False) if lang_meta else False
                        model_id = AYA_MODEL if is_regional else EUROLLM_MODEL
                        active_source = f"Stanza + {'Aya 32B' if is_regional else 'EuroLLM 22B'}"
                        
                        word_count = len(lemmas_to_translate)
                        # Number each lemma so the model knows exactly how many to return
                        numbered_input = "\n".join(f"{i+1}. {l}" for i, l in enumerate(lemmas_to_translate))
                        hf_headers = {"Authorization": f"Bearer {settings.HF_TOKEN}"}
                        sys_msg = (
                            f"You are a linguistic expert in {chat.language}. "
                            f"You will receive exactly {word_count} numbered {chat.language} words/lemmas. "
                            f"Translate each one into English. "
                            f"Reply with exactly {word_count} numbered lines in the format: '1. translation'. "
                            f"If you do not know the translation for a word, write '?' as the placeholder — do NOT skip that line. "
                            f"Do NOT add any extra text, commentary, or blank lines."
                        )
                        try:
                            print(f"DEBUG - Routing to {model_id} ({word_count} words)")
                            r = requests.post(HF_ROUTER_URL, headers=hf_headers, json={
                                "model": model_id,
                                "messages": [{"role": "system", "content": sys_msg},
                                             {"role": "user", "content": numbered_input}],
                                "max_tokens": 800, "temperature": 0.1
                            }, timeout=120)
                            if r.status_code == 200:
                                translated_bulk = r.json()['choices'][0]['message']['content'].strip()
                                print(f"DEBUG - Translated: {translated_bulk[:120]}...")
                                # Strip numbering: "1. be" → "be"
                                import re
                                translations = [re.sub(r'^\d+\.\s*', '', t).strip() for t in translated_bulk.split("\n") if t.strip()]
                                if len(translations) == len(lemmas_to_translate):
                                    for i, trans in enumerate(translations):
                                        if i < len(word_objects):
                                            word_objects[i]["translation"] = trans
                                else:
                                    print(f"DEBUG - Mismatch ({len(translations)} vs {len(lemmas_to_translate)}), sequential fallback.")
                                    for i, lemma in enumerate(lemmas_to_translate):
                                        if i >= len(word_objects): break
                                        r2 = requests.post(HF_ROUTER_URL, headers=hf_headers, json={
                                            "model": model_id,
                                            "messages": [{"role": "system", "content": f"Translate this {chat.language} word to English. Reply with ONLY the English word."},
                                                         {"role": "user", "content": lemma}],
                                            "max_tokens": 50, "temperature": 0.1
                                        }, timeout=45)
                                        if r2.status_code == 200:
                                            word_objects[i]["translation"] = r2.json()['choices'][0]['message']['content'].strip()
                            else:
                                print(f"DEBUG - {model_id} error {r.status_code}: {r.text}")
                                raise Exception(f"API Error {r.status_code}")
                        except Exception as e:
                            print(f"Translation failure ({model_id}): {repr(e)}")
                            for obj in word_objects:
                                if not obj["translation"]:
                                    obj["translation"] = "[error]"
                    else:
                        active_source = 'Stanza API'
                    
                    return JsonResponse({
                        'result': {"words": word_objects},
                        'is_structured': True,
                        'source': active_source
                    })
            except Exception as e:
                print(f"Stanza API error: {e}")

        # Fallback per Breton o se l'API Stanza fallisce
        prompt = (
            f"Provide a word-for-word literal translation of the following text into English (or {target_lang} if it is already in English). "
            f"Format the output as a JSON with 'words' as key containing a list of objects, each with 'form', 'lemma', 'grammar_comments', and 'translation'. "
            f"\n\nText: {text_to_analyze}"
        )
    elif action == "grammatical_explanation":
        prompt = (
            f"Provide a stylistic analysis of the following text in {target_lang}. "
            f"For each sentence, explain the stylistic choices, why it was said in that way, and identify if it uses colloquialisms, slang, or dialect. Also make sure to mention noteworthy grammatical structures and idiomatic expressions."
            f"\n\nText: {text_to_analyze}"
        )
    else:
        return JsonResponse({'error': 'Invalid action'}, status=400)

    try:
        is_structured = False
        if action == "word_to_word":
            parser = JsonOutputParser(pydantic_object=WordAnalysisList)
            full_prompt = prompt + "\n\n" + parser.get_format_instructions()
            messages = [
                SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
                HumanMessage(content=full_prompt)
            ]
            response_ai = current_util_model.invoke(messages)
            try:
                ai_text = parser.parse(response_ai.content)
                is_structured = True
            except Exception:
                ai_text = response_ai.content
        elif action == "grammatical_explanation":
            parser = JsonOutputParser(pydantic_object=StylisticAnalysis)
            full_prompt = prompt + "\n\n" + parser.get_format_instructions()
            messages = [
                SystemMessage(content="You are a highly capable linguistic assistant that always responds in valid JSON format as requested."),
                HumanMessage(content=full_prompt)
            ]
            response_ai = current_util_model.invoke(messages)
            try:
                ai_text = parser.parse(response_ai.content)
                is_structured = True
            except Exception:
                ai_text = response_ai.content
        elif action == "grammar_check":
            parser = JsonOutputParser(pydantic_object=GrammarAnalysis)
            full_prompt = prompt + "\n\n" + parser.get_format_instructions()
            messages = [
                SystemMessage(content="You are a linguistic expert assistant. Always respond in JSON format."),
                HumanMessage(content=full_prompt)
            ]
            response_ai = current_util_model.invoke(messages)
            try:
                ai_text = parser.parse(response_ai.content)
                is_structured = True
            except Exception:
                ai_text = response_ai.content
        elif action == "native_rewrite":
            parser = JsonOutputParser(pydantic_object=NativeRewriteAnalysis)
            full_prompt = prompt + "\n\n" + parser.get_format_instructions()
            messages = [
                SystemMessage(content="You are a native speaker linguistic assistant. Always respond in JSON format."),
                HumanMessage(content=full_prompt)
            ]
            response_ai = current_util_model.invoke(messages)
            try:
                ai_text = parser.parse(response_ai.content)
                is_structured = True
            except Exception:
                ai_text = response_ai.content
        elif action in ["translate", "translate_to_target"]:
            # Per le traduzioni in lingue piccole, usiamo direttamente MADLAD (Base LLM)
            if is_small_lang:
                target_code = "en" if action == "translate" else lang_info['code']
                madlad_prompt = f"<2{target_code}> {text_to_analyze}"
                try:
                    ai_text = llm_small_lang.invoke(madlad_prompt).strip()
                except Exception as e:
                    print(f"MADLAD Translate error: {e}")
                    # Fallback a modello base
                    messages = [
                        SystemMessage(content="You are a high-quality translator. Provide only the translation without any introduction."),
                        HumanMessage(content=prompt)
                    ]
                    response_ai = chat_model_util.invoke(messages)
                    ai_text = response_ai.content
            else:
                messages = [
                    SystemMessage(content="You are a high-quality translator. Provide only the translation without any introduction."),
                    HumanMessage(content=prompt)
                ]
                response_ai = chat_model_util.invoke(messages)
                ai_text = response_ai.content
        else:
            messages = [
                SystemMessage(content="You are a highly capable linguistic assistant."),
                HumanMessage(content=prompt)
            ]
            response_ai = current_util_model.invoke(messages)
            ai_text = response_ai.content

        # --- POST-PROCESSING per Lingue Piccole (MADLAD Overlay) ---
        if is_small_lang and is_structured:
            lang_code = lang_info['code'] if lang_info else "en"
            if action == "grammar_check" and isinstance(ai_text, dict) and "sentences" in ai_text:
                for s in ai_text["sentences"]:
                    if s.get("rewritten"):
                        try:
                            # Prompt con tag MADLAD: <2xx> per output nella lingua target
                            refine_prompt = f"<2{lang_code}> {s['rewritten']}"
                            refined_text = llm_small_lang.invoke(refine_prompt)
                            s["rewritten"] = refined_text.strip()
                        except Exception as e:
                            print(f"Refinement error: {e}")
            
            elif action == "native_rewrite" and isinstance(ai_text, dict) and "suggestions" in ai_text:
                for s in ai_text["suggestions"]:
                    if s.get("text"):
                        try:
                            refine_prompt = f"<2{lang_code}> {s['text']}"
                            refined_text = llm_small_lang.invoke(refine_prompt)
                            s["text"] = refined_text.strip()
                        except Exception as e:
                            print(f"Refinement error: {e}")

        return JsonResponse({
            'result': ai_text, 
            'is_structured': is_structured,
            'source': 'AI Model (Llama/Qwen)' if action == 'word_to_word' else None
        })
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)

# --- AI Logic ---

def send_to_telegram(chat, text):
    """Invia un messaggio tramite Telegram Bot API"""
    username = chat.user.username
    if not username.startswith("tg_"):
        return
    
    tg_id = username.replace("tg_", "")
    url = f"https://api.telegram.org/bot{settings.TELEGRAM_TOKEN}/sendMessage"
    
    payload = {
        "chat_id": tg_id,
        "text": f"*{chat.chat_name}*: \n\n{text[:15]}...",
        "parse_mode": "Markdown"
    }
    
    if chat.telegram_thread_id:
        payload["message_thread_id"] = chat.telegram_thread_id
        
    try:
        requests.post(url, json=payload, timeout=10)
    except Exception as e:
        print(f"Errore invio Telegram: {e}")

def process_chat_reply(chat):
    """
    Chiama l'AI e salva la risposta.
    """
    
    messages = chat.messages.all().order_by('-created_at')[:10][::-1]
    
    history = []
    for msg in messages:
        role = "user" if msg.is_user else "assistant"
        history.append({"role": role, "content": msg.content})
    
    try:
        # Construct LangChain message list
        langchain_messages = []
        if chat.summary:
            langchain_messages.append(SystemMessage(content=f"You are {chat.chat_name}, {chat.character_bio}, you speak {chat.language}. Remember, you are NOT a psychologist, not a lover, you only stick to your role where appropriate. Your main goal is to help the user learn {chat.language}. Remember to only speak in said language: {chat.language}.\nPrevious context: {chat.summary}"))
        else:
            langchain_messages.append(SystemMessage(content=f"You are {chat.chat_name}, {chat.character_bio}, you speak {chat.language}. Remember, you are NOT a psychologist, not a lover, you only stick to your role where appropriate. Your main goal is to help the user learn {chat.language}. Remember to only speak in said language: {chat.language}."))

        for msg in messages:
            if msg.is_user:
                langchain_messages.append(HumanMessage(content=msg.content))
            else:
                langchain_messages.append(AIMessage(content=msg.content))

        response = chat_model_main.invoke(langchain_messages)
        ai_content = response.content
        PendingMessage.objects.create(chat=chat, content=ai_content, is_user=False)
        
        # Notifica Telegram se il timer è >= 120 e l'utente è da Telegram
        if chat.reply_timer >= 120:
            send_to_telegram(chat, ai_content)
        
    except Exception as e:
        print(f"Error AI: {e}")
        PendingMessage.objects.create(chat=chat, content="Sorry, I'm having trouble thinking right now.", is_user=False)
@csrf_exempt
def tts_proxy(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            text = data.get("text", "")
            lang_name = data.get("lang", "English")
            
            # Look up lang code from SUPPORTED_LANGUAGES
            lang_code = "en"
            for l in SUPPORTED_LANGUAGES:
                if l['name'] == lang_name:
                    lang_code = l['code']
                    break
            
            url = "https://feliksius-ai-translation.hf.space/v1/tts"
            payload = {
                "input_text": text,
                "from_language": lang_code
            }
            
            response = requests.post(url, json=payload, timeout=20)
            if response.status_code == 200:
                # Return the audio as a wav response
                django_response = HttpResponse(response.content, content_type="audio/wav")
                django_response['Content-Disposition'] = 'inline; filename="speech.wav"'
                return django_response
            else:
                return JsonResponse({"error": f"TTS microservice error: {response.status_code}"}, status=response.status_code)
        except Exception as e:
            return JsonResponse({"error": str(e)}, status=500)
    return HttpResponse(status=405)