a
     ip                  
   @  s
  U d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZmZmZmZ ddlZddlmZmZmZ ddlmZmZ ddlmZmZmZmZ dd	lmZ ed
ddZdgZejeeddgdgd ejejdd ee Z!e"ddZ#e"ddZ$e%e"ddZ&e"dd' Z(e"dd' Z)e"de"dd' Z*e"dd' + Z,dZ-e.d  e.d!e-  e.d"ej/0e-  e.d#e1   ej/0e-rze2e-Z3e.d$e3  ej/4e-d%Z5ej/4e-d&Z6e.d'ej/0e5  e.d(ej/0e6  ej/0e6rVe7e6Z8e.d)e8j9 d* e.d+e:e8j;  W n4 e<y Z= ze.d,e=  W Y dZ=[=n
dZ=[=0 0 ne.d-e-  e.d. ed
d/d0d1Ze> Z?ej@jAd2d3d4d5ZBe?Cd6eB e?Cd7eB e	d8d9d:d; ZDi ZEd<eFd=< d>d?d@dAdBZGdCdDdEdFdGdHZHeIdIdJdK ZJeIdLdMdN ZKeLdOdPdQ ZMeIdRdSdT ZNeIdUdVdW ZOeIdXd>dYdZd[ZPeId\d]d^ ZQeId_d`da ZReIdbdcdd ZSeIded>dfdgdhZTeIdieddjdkeddjdkfd>dldldmdndoZUeIdpedqdrdkfdCdsdtduZVeIdvedqdrdkfdCdsdwdxZWG dydz dzZXdCdCd{d|d}ZYG d~d deZZG dd deZ[ej\dOe[dddddZ]eIdd>dYddZ^e_ddd Z`e dkrddlaZaeajbeddd dS )ux  
Assistant Gateway (FastAPI) — Moodle + RAG + OpenAI
Versão atualizada com correção definitiva do path INDEX_DIR

Funcionalidades:
- Endpoints Moodle (/courses, /course-contents, etc.)
- RAG com FAISS + SentenceTransformers (/ask)
- LLM integration (OpenAI compatible)
- Debug endpoints para diagnóstico
- Path absoluto fixo para resolver problemas de working directory
    )annotationsN)	lru_cache)AnyDictListOptional)FastAPIHTTPExceptionQuery)	BaseModel	validator)r   r	   r
   Response)CORSMiddlewarezAssistant Gatewayz1.0.0)titleversionzhttps://lms.ed-consulting.aoT*)Zallow_originsZallow_credentialsZallow_methodsZallow_headersz4%(asctime)s - %(name)s - %(levelname)s - %(message)s)levelformat
MOODLE_URLMOODLE_TOKEN REQUEST_TIMEOUTZ25MODEL_API_URLMODEL_API_KEY
MODEL_NAMEZOPENAI_MODELzgpt-4o-miniMODEL_PROVIDERZopenaiz/var/www/html/assistant/indexesz=== GATEWAY CONFIGURATION ===INDEX_DIR: INDEX_DIR exists: zCurrent working directory: zFiles in INDEX_DIR: zcourse_3.faisszcourse_3_meta.jsonzcourse_3.faiss exists: zcourse_3_meta.json exists: zcourse_3_meta.json size: z bytesz course_3_meta.json permissions: zError listing INDEX_DIR: zINDEX_DIR does not exist: z=== END CONFIGURATION ===1.2.0z0Moodle + RAG + LLM Gateway - Fixed Paths Version)r   r   description
         )Zpool_connectionsZpool_maxsizeZmax_retrieszhttp://zhttps://   )maxsizec               
   C  s   z.ddl m}  td | d}td |W S  tyd } ztd|   W Y d}~n>d}~0  ty } ztd|   W Y d}~n
d}~0 0 dS )	z:Cache do modelo SentenceTransformer para evitar recarregarr   SentenceTransformerz$Loading SentenceTransformer model...z&sentence-transformers/all-MiniLM-L6-v2z-SentenceTransformer model loaded successfullyz&Failed to import SentenceTransformer: Nz*Failed to load SentenceTransformer model: )sentence_transformersr&   loggerinfoImportErrorerror	Exception)r&   modele r/   "/var/www/html/assistant/gateway.pyget_sentence_transformerx   s    

r1   zDict[int, 'Retriever']retrievers_cacheintz'Retriever')courseidreturnc                 C  s,   | t vr$td|   t| t | < t |  S )u4   Cache dos retrievers para evitar recarregar índicesz"Creating new retriever for course )r2   r(   r)   	Retrieverr4   r/   r/   r0   get_retriever   s    r8   strDict[str, Any]r   )
wsfunctionparamsr5   c              
   C  s  t std tddt d}t d| d}zdtj|||td}|  |	 }t
|tr|drtd	|  td
d|dd |W S  tjy   td tddY nH tjy  } z,td|  tdd| W Y d}~n
d}~0 0 dS )z%Chama Moodle REST API e retorna JSON.zMOODLE_TOKEN not configured  u   MOODLE_TOKEN não configuradoz/webservice/rest/server.phpjson)ZwstokenZmoodlewsrestformatr;   )r<   datatimeout	exceptionzMoodle API error: i  zErro Moodle: messagezErro desconhecidozTimeout connecting to Moodlei  zTimeout ao contactar MoodlezMoodle request failed:   zFalha ao contactar Moodle: N)r   r(   r+   r	   r   sessionpostr   raise_for_statusr>   
isinstancedictgetrequestsZTimeoutRequestException)r;   r<   urlqsrr?   r.   r/   r/   r0   call_moodle   s,    



rO   z/healthc                   C  s    dt t dttjtdS )zHealth check endpointokr   )status	timestampr   	index_dirindex_dir_exists)r3   time	INDEX_DIRospathexistsr/   r/   r/   r0   health   s    

rZ   z/debug-configc                	   C  s6   t ttttjtttott	t
 tdj dS )z"Debug configuration (safe version)sys)Z
moodle_urlZhas_moodle_tokenrS   rT   Zhas_model_configZ
model_nameZ
python_cwdpython_version)r   boolr   rV   rW   rX   rY   r   r   r   getcwd
__import__r   r/   r/   r/   r0   debug_config   s    


r`   z/askc                   C  s
   t ddS )N   )status_code)r   r/   r/   r/   r0   options_ask   s    rc   z
/cors-testc                   C  s   dt t ddS )z!Endpoint simples para testar CORSu   CORS está funcionando!T)rB   rR   Zorigin_allowed)r3   rU   r/   r/   r/   r0   	cors_test   s    
rd   z/debug-routesc               
   C  sT   g } t jjD ]>}z&| |jttt|dg d W q tyH   Y q0 qd| iS )zList all available routesmethods)rX   re   routes)	appZrouterrf   appendrX   sortedlistgetattrr,   )rf   Zrouter/   r/   r0   debug_routes   s    rl   z/debug-rag/{courseid}r7   c                 C  sl  t jtd|  d}t jtd|  d}| t||t j|t j|t  d}d|fd|ffD ]\}}t j|rdzJt |}|j|| d< t|j	dd	 || d
< t 
|t j|| d< |j|| d< |dkrzt|ddd}| }t||d< t|dkr*|d	d d n||d< t|}	d|d< t|	j|d< t|	ttfrnt|	|d< W d	   n1 s0    Y  W n2 ty }
 zt|
|d< W Y d	}
~
n
d	}
~
0 0 W qd ty } zt||| d< W Y d	}~qdd	}~0 0 qdi }z"dd	l}dt|ddd|d< W n8 ty` } zd t|d!|d< W Y d	}~n
d	}~0 0 zdd"lm} d#di|d$< W n8 ty } zd t|d!|d$< W Y d	}~n
d	}~0 0 ||d%< zlt| }| |d&< | r|j d'd(d)}t||d*< |r|d |d+< d,|d-< n|! |d.< d/|d-< W n: tyf } z t||d0< d1|d-< W Y d	}~n
d	}~0 0 |S )2z6Debug endpoint ATUALIZADO para verificar status do RAGcourse_.faiss
.meta.json)r4   rS   
index_path	meta_pathindex_existsmeta_existscwdindexmeta_sizeNZ_permissions	_readableZ	_modifiedrN   utf-8encodingZmeta_content_length   ...Zmeta_previewTZmeta_json_validZ	meta_typeZ
meta_itemsZmeta_read_error_errorr   OK__version__unknown)rQ   r   faissERROR)rQ   r+   r%   rQ   r'   ZdependenciesZretriever_okz
test queryr#   kZtest_search_countZtest_search_sampleSUCCESSZretriever_statusZretriever_errorZFAILEDZretriever_exceptionZ	EXCEPTION)"rW   rX   joinrV   rY   r^   statst_sizeoctst_modeaccessR_OKst_mtimeopenreadlenr>   loadstype__name__rG   rj   rH   r,   r9   r   rk   r*   r'   r&   r8   rP   search
last_error)r4   rp   rq   r)   Zpath_keyZ	file_path	stat_infofcontentZ	meta_dataZ
read_errorr.   Zdepsr   r&   	retrieverZtest_resultsr/   r/   r0   	debug_rag   sx    



&
0&*((

r   z
/site-infoc                   C  s
   t di S )zGet Moodle site informationZcore_webservice_get_site_inforO   r/   r/   r/   r0   	site_infoD  s    r   z/coursesc                   C  s
   t di S )zGet all coursescore_course_get_coursesr   r/   r/   r/   r0   coursesI  s    r   z/courses_slimc                  C  s   t di } dd | D S )z&Get courses with essential fields onlyr   c                 S  s*   g | ]"}| d | d| ddqS )id	shortnamefullname)r   r   r   )rI   ).0cr/   r/   r0   
<listcomp>R  s
   z courses_slim.<locals>.<listcomp>r   )r?   r/   r/   r0   courses_slimN  s    
r   z/user-coursesuseridc                 C  s   t dd| iS )zGet courses for a specific userZcore_enrol_get_users_coursesr   r   r   r/   r/   r0   user_courses[  s    r   z/course-contentsz
true/false)r   zOptional[str])r4   includecontentsexcludemodulesc                 C  sn   d| i}d}|dur<d|d| d< ||d| d< |d7 }|durdd	|d| d< ||d| d< t d
|S )z Get course contents with optionsr4   r   Nr   zoptions[z][name]z][value]r#   r   Zcore_course_get_contentsr   )r4   r   r   optsidxr/   r/   r0   course_contents`  s    r   z/assignments.zCourse IDs separated by comma)	courseidsc                 C  sJ   dd t d|  D }i }t|D ]\}}||d| d< q&td|S )zGet assignments for coursesc                 S  s   g | ]}|rt |qS r/   r3   r   xr/   r/   r0   r   x      zassignments.<locals>.<listcomp>[,\s]+z
courseids[]Zmod_assign_get_assignmentsresplitstrip	enumeraterO   r   Zidsr<   iZcidr/   r/   r0   assignmentsu  s
    r   z/calendar/eventsc                 C  sZ   dd t d|  D }i }t|D ](\}}d|d| d< ||d| d< q&td|S )	zGet calendar events for coursesc                 S  s   g | ]}|rt |qS r/   r   r   r/   r/   r0   r     r   z#calendar_events.<locals>.<listcomp>r   Zcoursezevents[events][z][eventtype]z][courseids][0]Z!core_calendar_get_calendar_eventsr   r   r/   r/   r0   calendar_events~  s    r   c                   @  s^   e Zd ZdZddddZdd Zdd	 Zd
dddZddddZdddddddZ	dS )r6   zA
    Retriever corrigido com path absoluto e logs detalhados
    r3   r7   c                 C  s   t || _tjtd| j d| _tjtd| j d| _d| _d | _	t
d| d t
d| j  t
d| j  |   d S )	Nrm   rn   ro   Fz&*** INITIALIZING RETRIEVER FOR COURSE  ***zIndex path: zMeta path: )r3   r4   rW   rX   r   rV   rp   rq   _init_ok	_init_errr(   r)   _initialize)selfr4   r/   r/   r0   __init__  s    
zRetriever.__init__c              
   C  s&  zddl }td td| j  tj| j}td|  td| j  tj| j}td|  |std| j |std	| j t	| jtj
std
| j t	| jtj
std| j td td || _ || j| _td| jj d td t | _td td t| jddd}t|}W d   n1 st0    Y  tdt|  | | d| _td| j d W nh ty  } zNd| _|| _td| j d| d ddl}td|   W Y d}~n
d}~0 0 dS )u:   Initialize com logging detalhado e verificações robustasr   NzFAISS imported successfullyzChecking if index exists: zIndex exists: zChecking if meta exists: zMeta exists: zFAISS index file not found: zMetadata file not found: zCannot read index file: zCannot read metadata file: zFile permissions OKzLoading FAISS index...z!FAISS index loaded successfully: z vectorsz$Getting SentenceTransformer model...zSentenceTransformer readyzLoading metadata file...rN   rz   r{   zMetadata loaded: type=Tz?*** RETRIEVER INITIALIZATION COMPLETED SUCCESSFULLY FOR COURSE r   Fz/*** RETRIEVER INITIALIZATION FAILED FOR COURSE z: Full traceback:
)r   r(   r)   rp   rW   rX   rY   rq   FileNotFoundErrorr   r   PermissionErrorZ
read_indexru   Zntotalr1   embedr   r>   loadr   _process_metadatar   r4   r,   r   r+   	traceback
format_exc)r   r   rr   rs   r   rv   r.   r   r/   r/   r0   r     sL    





*
zRetriever._initializec              	     s   t  tr< | _dd t D | _tdt  d nt  trڈ | _zPt	dd  
 D } fddt|d	 D | _td
t  d|  W q ttfy   t  | _td
t  d Y q0 n g | _i | _tdt   dS )z%Process and normalize metadata formatc                 S  s   i | ]\}}t ||qS r/   )r9   )r   r   itemr/   r/   r0   
<dictcomp>  r   z/Retriever._process_metadata.<locals>.<dictcomp>z%Metadata processed: list format with z itemsc                 s  s   | ]}|  rt|V  qd S )N)isdigitr3   )r   r   r/   r/   r0   	<genexpr>  r   z.Retriever._process_metadata.<locals>.<genexpr>c                   s   g | ]}  t|i qS r/   )rI   r9   )r   r   rv   r/   r0   r     r   z/Retriever._process_metadata.<locals>.<listcomp>r#   z%Metadata processed: dict format with z keys, max_index=z keys, non-numeric indiceszUnknown metadata format: N)rG   rj   	meta_listr   	meta_dictr(   r)   r   rH   maxkeysrange
ValueError	TypeErrorvalueswarningr   )r   rv   Zmax_keyr/   r   r0   r     s     

zRetriever._process_metadatar]   )r5   c                 C  s   | j S )z*Check if retriever is properly initialized)r   r   r/   r/   r0   rP     s    zRetriever.okr9   c                 C  s   | j rt| j S dS )zGet last initialization errorzNo error)r   r9   r   r/   r/   r0   r     s    zRetriever.last_error   List[Dict[str, Any]])questionr   r5   c                 C  s6  | j std|   td|dd  d| d | j|g}| j||\}}g }t	|d D ]\}}|dkrzqhd}	|t
| jk r| j| }	nt|| jv r| jt| }	|	st|d	| d
d}	t|	}
|
|t|t|d | tddt|d |  d ||
 qhtdt
| d |S )z"Search for top-k relevant passageszRetriever not initialized: zSearching for: 'N2   z...' (k=)r   zChunk z (no metadata))r   textg      ?)rankZ	faiss_idxZdistanceZ
similarityzSearch completed: found z results)r   RuntimeErrorr   r(   r)   r   encoderu   r   r   r   r   r9   r   r3   rH   updatefloatr   rh   )r   r   r   ZqvDIresultsr   r   r   resultr/   r/   r0   r     s4     zRetriever.searchN)r   )
r   
__module____qualname____doc__r   r   r   rP   r   r   r/   r/   r/   r0   r6     s   <r6   )promptr5   c              
   C  s  t rtrtstd dS dt dd}tdddd	| dgd
dd}ztdt   tjt ||td}|	  |
 }|di gd di dd }|std W dS tdt| d |W S  tjy } z,td|  tdd| W Y d}~n
d}~0 0 dS )zCall OpenAI-compatible LLM APIz0LLM not configured, returning simulated responseuw   Resposta simulada (LLM não configurado). Configure MODEL_API_URL, MODEL_API_KEY e MODEL_NAME para usar um modelo real.zBearer zapplication/json)AuthorizationzContent-Typesystemug   Você é um assistente educacional. Responda com base nos trechos fornecidos de forma clara e objetiva.)roler   userg?i  )r-   messagesZtemperatureZ
max_tokenszCalling LLM API: )headersr>   r@   choicesr   rB   r   r   zEmpty response from LLMzResposta vazia do modelo.zLLM response received: z charszLLM API call failed: rC   zFalha ao contactar modelo: N)r   r   r   r(   r   r)   rD   rE   r   rF   r>   rI   r   r   rJ   rK   r+   r	   )r   r   payloadrN   r?   answerr.   r/   r/   r0   call_llm_openai!  s>    

&
r   c                   @  sZ   e Zd ZU ded< ded< dZded< eddd	 Zedd
d Zeddd ZdS )
AskRequestr9   r   r3   r4   r   zOptional[int]top_kc                 C  s0   |r|  stdt|dkr(td|  S )Nu   Pergunta não pode estar vaziai  u.   Pergunta muito longa (máximo 2000 caracteres))r   r   r   clsvr/   r/   r0   validate_questionW  s
    zAskRequest.validate_questionc                 C  s   |dkrt d|S )Nr   zCourse ID deve ser positivor   r   r/   r/   r0   validate_courseid_  s    zAskRequest.validate_courseidc                 C  s$   |d ur |dk s|dkr t d|S )Nr#   r!   ztop_k deve estar entre 1 e 20r  r   r/   r/   r0   validate_top_ke  s    zAskRequest.validate_top_kN)	r   r   r   __annotations__r   r   r   r  r  r/   r/   r/   r0   r   R  s   


r   c                   @  s6   e Zd ZU ded< ded< ded< ded< ded	< d
S )AskResponser9   r   r3   r4   r   passagesr   r:   metadataN)r   r   r   r  r/   r/   r/   r0   r  k  s
   
r  )Zresponse_model)requestc                 C  s  t   }td td| jdd  d td| j  td| j  z*td t| j}| sd	| j d
|  d| j }t	| t
d|td ttd| jpdd}|j| j|d}tdt| d |s td t| j| jg dt   | dddW S td g }t|d| dD ]\}}|dpn|dpn|dpnd}	|dp|dp|d pd!| }
|	 r@|d"| d#|	  d$|
 d% q@|st| j| j|d&t   | d'ddW S d(|}d)| j d*| d+}td, t|}t   | }td-|d.d/ t| j| j|||t|t|trptnd0d1d2dW S  t
y    Y nb ty } zHt	d3|  d4dl}t	d5|   t
dd6| W Y d}~n
d}~0 0 dS )7zJ
    RAG endpoint atualizado com melhor logging e tratamento de erros
    z=== PROCESSING ASK REQUEST ===z
Question: Nd   r~   zCourse ID: zTop K: zGetting retriever...u"   RAG não disponível para o curso z. Erro: u3   . Para gerar o índice, execute: python3 ingest.py r=   z"Retriever OK, performing search...r#   r   r!   r   zSearch completed: z passages foundzNo relevant passages founduI   Não foram encontrados conteúdos relevantes para esta pergunta no curso.Z
no_content)processing_timerQ   )r   r4   r  r   r  zBuilding context for LLM...r   r   Zsnippetr   sourcefilerL   z
Documento u   [CONTEÚDO z]
z	
(Fonte: z)
uR   Os conteúdos encontrados não possuem texto relevante para responder à pergunta.Zno_text
z
Pergunta: u[   

Com base nos seguintes conteúdos do curso, forneça uma resposta educativa e completa:

u'  

Instruções para a resposta:
- Use apenas as informações fornecidas nos conteúdos acima
- Seja claro, educativo e bem estruturado
- Cite as fontes quando relevante (Fonte: ...)
- Se a informação não estiver completa nos conteúdos, mencione isso
- Organize a resposta de forma didáticazGenerating LLM response...z=== REQUEST COMPLETED IN z.2fzs ===Z	simulatedZsuccess)r
  Zpassages_foundZcontext_lengthZ
model_usedrQ   zUnexpected error in /ask: r   r   zErro interno: )rU   r(   r)   r   r4   r   r8   rP   r   r+   r	   minr   r   r   r   r  r   rI   r   rh   r   r   r   r,   r   r   )r  Z
start_timer   Z	error_msgr   r  Zcontext_partsr   Zpassager   r  contextr   r   r
  r.   r   r/   r/   r0   asku  s    







	
(.
&


r  z/courses/{courseid}/rag-statusc              
   C  s   t jtd|  d}t jtd|  d}| dt j|t j|dtd}|d r|d rzJt| }| |d	< tt j|t j||d
< |d	 s|	 |d< W n0 t
y } zt||d< W Y d}~n
d}~0 0 |S )z,Check RAG availability for a specific courserm   rn   ro   FN)r4   rag_availablerr   rs   last_modifiedrS   rr   rs   r  r  r+   )rW   rX   r   rV   rY   r8   rP   r   getmtimer   r,   r9   )r4   rp   rq   rQ   r   r.   r/   r/   r0   
rag_status  s,    

	

"r  startupc               
     s  t d t d t dt  t dtjt  t dt  t dtt  t dtt	ont
  t dt   tjtrz,tt} d	d
 | D }t d|  W n4 ty } zt d|  W Y d}~n
d}~0 0 t d dS )zApplication startup eventz"=== ASSISTANT GATEWAY STARTING ===zVersion: 1.2.0r   r   zMOODLE_URL: zHas MOODLE_TOKEN: zHas LLM config: zWorking Directory: c                 S  s.   g | ]&}| d r|ds&|dr|qS )rm   rn   z
_meta.json)
startswithendswith)r   r   r/   r/   r0   r     r   z!startup_event.<locals>.<listcomp>zAvailable course indexes: z#Could not list INDEX_DIR contents: Nz=== STARTUP COMPLETE ===)r(   r)   rV   rW   rX   rY   r   r]   r   r   r   r^   listdirr,   r   )filesZcourse_filesr.   r/   r/   r0   startup_event  s     


&r  __main__z	127.0.0.1i@  )hostport)cr   
__future__r   rW   r>   rU   r   logging	functoolsr   typingr   r   r   r   rJ   Zfastapir   r	   r
   Zpydanticr   r   r   Zfastapi.middleware.corsr   rg   ZALLOWED_ORIGINSZadd_middlewarebasicConfigINFO	getLoggerr   r(   getenvr   r   r3   r   r   r   r   r   lowerr   rV   printrX   rY   r^   r  r  r   Zcourse_3_faissZcourse_3_metar   r   r   r   r   r,   r.   ZSessionrD   ZadaptersZHTTPAdapterZadapterZmountr1   r2   r  r8   rO   rI   rZ   r`   optionsrc   rd   rl   r   r   r   r   r   r   r   r   r6   r   r   r  rE   r  r  Zon_eventr  uvicornrunr/   r/   r/   r0   <module>   s   


&	

"





V




 1
m 

