mirror of
https://github.com/mcloh/poc-grok-websearch.git
synced 2026-11-05 22:04:12 +00:00
Add OCI Grok web-search chatbot example and selected validation evidence
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
.venv
|
||||
evidence
|
||||
__pycache__
|
||||
.git
|
||||
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
OCI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1
|
||||
OCI_API_KEY=replace-with-oci-generative-ai-api-key
|
||||
OCI_PROJECT_ID=replace-with-active-chicago-project-id
|
||||
APP_PASSWORD=replace-with-a-strong-password
|
||||
APP_BASE_URL=http://localhost:8080/
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Credenciais e ambientes locais
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
# Diagnósticos e configuração específicos do ambiente
|
||||
deploy.py
|
||||
browser_check.py
|
||||
render_check.py
|
||||
# Compartilhar somente evidências selecionadas
|
||||
evidence/*
|
||||
!evidence/RELATORIO.md
|
||||
!evidence/validation-summary.json
|
||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py .
|
||||
COPY static static
|
||||
USER 65534:65534
|
||||
CMD ["uvicorn","app:app","--host","0.0.0.0","--port","8080","--root-path","/grok-poc"]
|
||||
131
README.md
Normal file
131
README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Grok Web Search via OCI
|
||||
|
||||
POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa `xai.grok-4.3` pela OCI Generative AI e habilita a ferramenta nativa `web_search` na Responses API. A aplicação chama exclusivamente o endpoint OCI.
|
||||
|
||||
## Estrutura
|
||||
|
||||
```text
|
||||
.
|
||||
├── app.py # Backend e autenticação
|
||||
├── static/
|
||||
│ ├── index.html # Interface e estilos
|
||||
│ └── app.js # Chat, histórico e citações
|
||||
├── requirements.txt
|
||||
├── Dockerfile
|
||||
├── .dockerignore
|
||||
├── .gitignore
|
||||
├── .env.example # Variáveis sem credenciais
|
||||
├── validate.py # Validação REST com inferência real
|
||||
└── evidence/
|
||||
├── RELATORIO.md # Resultados da POC
|
||||
└── validation-summary.json # Evidências selecionadas
|
||||
```
|
||||
|
||||
## Configuração
|
||||
|
||||
Requer Python 3.12 ou Docker, uma chave OCI Generative AI e um projeto GenAI ativo na região `us-chicago-1`, com permissão para inferência e Responses API.
|
||||
|
||||
Na raiz do repositório:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
Preencha `.env`:
|
||||
|
||||
| Variável | Finalidade |
|
||||
|---|---|
|
||||
| `OCI_BASE_URL` | `https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1` |
|
||||
| `OCI_API_KEY` | Chave OCI Generative AI |
|
||||
| `OCI_PROJECT_ID` | Identificador de projeto ativo em Chicago |
|
||||
| `APP_PASSWORD` | Senha forte para o usuário `poc` |
|
||||
| `APP_BASE_URL` | URL pública ou local, com o prefixo de publicação; usada por validate.py |
|
||||
|
||||
`.env` é ignorado pelo Git e pelo build Docker. As credenciais OCI ficam no backend. O histórico de conversa permanece na página e é reenviado a cada turno.
|
||||
|
||||
## Executar localmente
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
uvicorn app:app --host 127.0.0.1 --port 8080
|
||||
```
|
||||
|
||||
Abra `http://localhost:8080/`. A autenticação solicita usuário `poc` e a senha definida em `APP_PASSWORD`.
|
||||
|
||||
Com Docker:
|
||||
|
||||
```bash
|
||||
docker build -t poc-grok-websearch:1 .
|
||||
docker run -d --name poc-grok-websearch --restart unless-stopped \
|
||||
--env-file .env -p 127.0.0.1:8080:8080 poc-grok-websearch:1 \
|
||||
uvicorn app:app --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## Publicar na VM
|
||||
|
||||
Copie o repositório e configure `.env` na VM. Faça o build com o [Dockerfile](Dockerfile) e execute o container em uma rede acessível ao proxy reverso. Publique por HTTPS, encaminhando para a porta interna `8080`.
|
||||
|
||||
O comando padrão da imagem configura `--root-path /grok-poc`. Para esse modo, publique em `/grok-poc/` e remova o prefixo `/grok-poc` no proxy antes de encaminhar ao backend. Para publicação na raiz do domínio, use o comando uvicorn sem `--root-path`, como no exemplo Docker acima. Ajuste `APP_BASE_URL` para a URL completa, incluindo o prefixo quando usado.
|
||||
|
||||
Basic Auth requer HTTPS fora do ambiente local. A POC limita a concorrência a duas inferências, mas não impõe limite financeiro por usuário nem rate limiting por cliente.
|
||||
|
||||
## Arquitetura
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI[Browser / JavaScript] -->|HTTPS + Basic Auth| Proxy[Proxy reverso na VM]
|
||||
Proxy --> API[Python / FastAPI]
|
||||
API -->|Bearer OCI + OpenAI-Project| OCI[OCI Chicago /v1/responses]
|
||||
OCI --> Grok[xai.grok-4.3 + web_search]
|
||||
Grok --> Web[Pesquisa e abertura de páginas]
|
||||
```
|
||||
|
||||
O backend envia `tools: [{"type":"web_search"}]`, `store:false` e limite de 2400 tokens de saída. O modelo decide quando pesquisar e pode reformular consultas. Não há fallback para outros provedores nem motor de busca externo implementado pela aplicação.
|
||||
|
||||
A Oracle informa que os modelos Grok acessados pela OCI são hospedados externamente pela xAI. A aplicação autentica e chama exclusivamente OCI. Veja a [documentação Oracle](https://docs.oracle.com/en-us/iaas/Content/generative-ai/api-keys.htm).
|
||||
|
||||
Nos testes de 14/10/2026, `/openai/v1/responses` rejeitou `web_search` para Grok; `/v1/responses` e `/20231130/actions/v1/responses` funcionaram com um projeto ativo de Chicago. A aplicação usa `/v1/responses`.
|
||||
|
||||
## API REST
|
||||
|
||||
`POST /api/chat` com HTTP Basic. Na publicação com prefixo, use `POST /grok-poc/api/chat`.
|
||||
|
||||
```json
|
||||
{"message":"Pesquise uma notícia publicada hoje e cite fonte e data.","history":[]}
|
||||
```
|
||||
|
||||
`history` aceita até 12 turnos com `role` igual a `user` ou `assistant`; cada conteúdo tem limite de 8000 caracteres. A resposta contém `answer`, `citations`, `web_search_calls`, `usage`, `response_id`, `model`, `timestamp`, `seconds` e `raw_response`.
|
||||
|
||||
Exemplo local; curl solicita a senha:
|
||||
|
||||
```bash
|
||||
curl --user poc http://localhost:8080/api/chat \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"message":"Pesquise uma notícia publicada hoje e cite fonte e data."}'
|
||||
```
|
||||
|
||||
`GET /health` retorna o estado do serviço sem autenticação. A página, JavaScript e chat exigem autenticação.
|
||||
|
||||
## Validar
|
||||
|
||||
Com o serviço em execução e `.env` preenchido:
|
||||
|
||||
```bash
|
||||
. .venv/bin/activate
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
python validate.py
|
||||
```
|
||||
|
||||
O [script](validate.py) faz uma chamada paga de inferência sobre notícia de hoje, verifica resposta, eventos de busca e citações, além dos códigos HTTP de autenticação e validação. Salva resultados locais em `./evidence/`, ignorados pelo Git. A confirmação da data e do conteúdo da fonte requer conferência independente; o script não a automatiza.
|
||||
|
||||
Na execução original, o REST retornou HTTP 200 em 36,12 segundos, com duas buscas concluídas e uma abertura de página após uma tentativa inicial falhar. A fonte foi acessada independentemente e mostrou publicação em 14/10/2026 às 08:17. Desktop e celular passaram nas verificações de JavaScript e layout. A renderização foi verificada com uma fixture da resposta REST real.
|
||||
|
||||
Consulte o [relatório](evidence/RELATORIO.md) e o [resumo estruturado](evidence/validation-summary.json). Apenas evidências selecionadas são compartilhadas; credenciais, dados administrativos OCI, respostas brutas, cópias integrais das fontes e scripts específicos da infraestrutura ficam locais.
|
||||
50
app.py
Normal file
50
app.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import os,time,secrets,asyncio
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
from fastapi import FastAPI,HTTPException,Depends
|
||||
from fastapi.security import HTTPBasic,HTTPBasicCredentials
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel,Field
|
||||
|
||||
app=FastAPI(title='Grok OCI · Web Search',docs_url=None,redoc_url=None)
|
||||
auth=HTTPBasic()
|
||||
def authorize(c:HTTPBasicCredentials=Depends(auth)):
|
||||
if not (secrets.compare_digest(c.username,'poc') and secrets.compare_digest(c.password,os.environ['APP_PASSWORD'])):
|
||||
raise HTTPException(401,'Acesso restrito',headers={'WWW-Authenticate':'Basic'})
|
||||
class Turn(BaseModel):
|
||||
role:str
|
||||
content:str=Field(max_length=8000)
|
||||
class Chat(BaseModel):
|
||||
message:str=Field(min_length=1,max_length=8000)
|
||||
history:list[Turn]=Field(default_factory=list,max_length=12)
|
||||
@app.get('/health')
|
||||
def health():return {'status':'ok','provider':'OCI','model':'xai.grok-4.3','tool':'web_search'}
|
||||
@app.get('/',dependencies=[Depends(authorize)])
|
||||
def index():return FileResponse('static/index.html')
|
||||
@app.get('/app.js',dependencies=[Depends(authorize)])
|
||||
def js():return FileResponse('static/app.js',media_type='application/javascript')
|
||||
lock=asyncio.Semaphore(2)
|
||||
@app.post('/api/chat',dependencies=[Depends(authorize)])
|
||||
async def chat(q:Chat):
|
||||
if any(t.role not in ('user','assistant') for t in q.history):raise HTTPException(422,'Papel inválido')
|
||||
base=os.environ['OCI_BASE_URL'].rstrip('/')
|
||||
if urlparse(base).hostname!='inference.generativeai.us-chicago-1.oci.oraclecloud.com':raise HTTPException(503,'Endpoint OCI inválido')
|
||||
now=datetime.now(ZoneInfo('America/Sao_Paulo'))
|
||||
body={'model':'xai.grok-4.3','input':[t.model_dump() for t in q.history]+[{'role':'user','content':q.message}],
|
||||
'instructions':f'Você é um assistente em português brasileiro. Hoje é {now.isoformat()}. Use a ferramenta web_search para verificar informações atuais. Cite fontes com URLs e datas. Diferencie data de publicação e data do fato. Nunca invente notícias; se não houver evidência de hoje, diga isso claramente.',
|
||||
'tools':[{'type':'web_search'}],'store':False,'max_output_tokens':2400}
|
||||
started=time.monotonic()
|
||||
async with lock:
|
||||
try:r=await asyncio.to_thread(requests.post,base+'/responses',json=body,headers={'Authorization':'Bearer '+os.environ['OCI_API_KEY'],'OpenAI-Project':os.environ['OCI_PROJECT_ID']},timeout=(10,150))
|
||||
except requests.RequestException:raise HTTPException(502,'Falha de conexão com OCI')
|
||||
if r.status_code!=200:raise HTTPException(502,{'message':'OCI recusou a chamada','provider_status':r.status_code})
|
||||
d=r.json();texts=[];citations=[];calls=[]
|
||||
for item in d.get('output',[]):
|
||||
if item.get('type')=='web_search_call':calls.append(item)
|
||||
for c in item.get('content',[]):
|
||||
if c.get('text'):texts.append(c['text'])
|
||||
for a in c.get('annotations',[]):
|
||||
if a.get('type')=='url_citation':citations.append(a)
|
||||
return {'answer':'\n'.join(texts),'citations':citations,'web_search_calls':calls,'usage':d.get('usage'),'response_id':d.get('id'),'model':d.get('model'),'provider':'OCI','timestamp':now.isoformat(),'seconds':round(time.monotonic()-started,2),'raw_response':d}
|
||||
21
evidence/RELATORIO.md
Normal file
21
evidence/RELATORIO.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Validação — 14/10/2026
|
||||
|
||||
A POC foi publicada em uma VM OCI e testada por HTTPS. O resumo compartilhável está em [validation-summary.json](validation-summary.json).
|
||||
|
||||
| Critério | Resultado |
|
||||
|---|---|
|
||||
| REST sobre notícia de hoje | HTTP 200; 36,12 segundos |
|
||||
| Serviço e modelo | OCI Chicago; xai.grok-4.3 |
|
||||
| Busca nativa | Duas buscas concluídas e uma abertura de página; uma tentativa inicial falhou |
|
||||
| Citações | Duas citações da mesma notícia |
|
||||
| Fonte verificada independentemente | HTTP 200; data visível 14/10/2026 às 08:17 |
|
||||
| Acesso não autenticado | HTTP 401 |
|
||||
| Histórico com papel inválido | HTTP 422 |
|
||||
| Desktop e celular | Sem erros de JavaScript e sem overflow horizontal |
|
||||
| Renderização | Resposta REST real usada como fixture; texto e links conferidos |
|
||||
|
||||
Fonte: [Agência Brasil — Confira a agenda dos presidenciáveis nesta quarta-feira (14)](https://agenciabrasil.ebc.com.br/politica/noticia/2026-10/confira-agenda-dos-presidenciaveis-nesta-quarta-feira-14). A data coincide no texto visível e no metadado; o horário difere (08:17 no texto e 11:17 no metadado). O resumo registra o horário visível.
|
||||
|
||||
A ferramenta executou uma busca inicial que falhou, reformulou consultas e concluiu a pesquisa. As respostas brutas, identificadores OCI, capturas e cópia integral da fonte permanecem locais e não são versionados. O SHA-256 da fonte permite conferir o artefato local.
|
||||
|
||||
A chamada final consumiu 15.544 tokens. Incluindo os diagnósticos bem-sucedidos, foram 34.829 tokens e nove ferramentas contabilizadas pelo serviço. A escala financeira de cost_in_usd_ticks não foi verificada; o campo é preservado sem conversão para dólares.
|
||||
78
evidence/validation-summary.json
Normal file
78
evidence/validation-summary.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"tested_at": "2026-10-14T10:34:33.339815-03:00",
|
||||
"http": 200,
|
||||
"seconds": 37.25,
|
||||
"provider": "OCI",
|
||||
"model": "xai.grok-4.3",
|
||||
"web_search_calls": [
|
||||
{
|
||||
"type": "search",
|
||||
"status": "failed"
|
||||
},
|
||||
{
|
||||
"type": "search",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"type": "search",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"type": "open_page",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"citation_count": 2,
|
||||
"source": {
|
||||
"url": "https://agenciabrasil.ebc.com.br/politica/noticia/2026-10/confira-agenda-dos-presidenciaveis-nesta-quarta-feira-14",
|
||||
"http": 200,
|
||||
"visible_publication": "14/10/2026 - 08:17",
|
||||
"html_sha256": "f7642c836aa347cf4d04ea68eb2f24d3888172669ee4be1099b8f5c5cec7d12b"
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": 14359,
|
||||
"input_tokens_details": {
|
||||
"cached_tokens": 5760
|
||||
},
|
||||
"output_tokens": 1185,
|
||||
"output_tokens_details": {
|
||||
"reasoning_tokens": 798
|
||||
},
|
||||
"total_tokens": 15544,
|
||||
"num_sources_used": 0,
|
||||
"num_server_side_tools_used": 3,
|
||||
"cost_in_usd_ticks": 298632500,
|
||||
"server_side_tool_usage_details": {
|
||||
"web_search_calls": 3,
|
||||
"x_search_calls": 0,
|
||||
"x_posts_fetched": 0,
|
||||
"x_users_fetched": 0,
|
||||
"code_interpreter_calls": 0,
|
||||
"file_search_calls": 0,
|
||||
"mcp_calls": 0,
|
||||
"document_search_calls": 0,
|
||||
"image_generation_calls": 0
|
||||
},
|
||||
"context_details": {
|
||||
"input_tokens": 7565,
|
||||
"output_tokens": 466
|
||||
}
|
||||
},
|
||||
"http_checks": {
|
||||
"index": 200,
|
||||
"app.js": 200,
|
||||
"health": 200,
|
||||
"unauthenticated_post": 401,
|
||||
"invalid_history": 422
|
||||
},
|
||||
"browser_checks": {
|
||||
"http": 200,
|
||||
"js_errors": [],
|
||||
"desktop": true,
|
||||
"mobile_no_overflow": true
|
||||
},
|
||||
"render_check": {
|
||||
"answer_rendered": true,
|
||||
"method": "Fixture da resposta REST real previamente coletada, sem nova inferência"
|
||||
}
|
||||
}
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi==0.119.0
|
||||
uvicorn==0.37.0
|
||||
requests==2.32.5
|
||||
4
static/app.js
Normal file
4
static/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const form=document.querySelector('#form'),box=document.querySelector('#messages'),prompt=document.querySelector('#prompt'),status=document.querySelector('#status'),send=document.querySelector('#send');let history=[];
|
||||
function formatAnswer(div,text){div.textContent='';const pattern=/\*\*([^*]+)\*\*|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;let last=0;for(const m of text.matchAll(pattern)){div.append(document.createTextNode(text.slice(last,m.index)));const el=document.createElement(m[1]?'strong':'a');el.textContent=m[1]||m[2];if(!m[1]){el.href=m[3];el.target='_blank';el.rel='noopener noreferrer';}div.append(el);last=m.index+m[0].length;}div.append(document.createTextNode(text.slice(last)));}
|
||||
function turn(text,role){const div=document.createElement('div');div.className='turn '+role;div.textContent=text;box.append(div);return div;}
|
||||
form.addEventListener('submit',async e=>{e.preventDefault();const message=prompt.value.trim();if(!message)return;turn(message,'user');prompt.value='';send.disabled=true;status.textContent='Consultando Grok e pesquisando na web…';try{const r=await fetch('./api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message,history:history.slice(-12)})});if(!r.ok)throw Error(`Erro HTTP ${r.status}`);const d=await r.json();const div=turn(d.answer||'Sem texto na resposta.','assistant');formatAnswer(div,d.answer||'Sem texto na resposta.');const meta=document.createElement('div');meta.className='meta';meta.textContent=`${d.seconds}s · ${d.web_search_calls.length} chamadas de busca · ${d.citations.length} citações`;div.append(meta);for(const c of d.citations){try{const url=new URL(c.url);if(!['https:','http:'].includes(url.protocol))continue;const a=document.createElement('a');a.href=url.href;a.target='_blank';a.rel='noopener noreferrer';a.textContent=c.title && c.title!=='1'?c.title:url.hostname;div.append(document.createElement('br'),a);}catch{}}const details=document.createElement('details');const summary=document.createElement('summary');summary.textContent='Evidências da ferramenta';const pre=document.createElement('pre');pre.textContent=JSON.stringify({response_id:d.response_id,usage:d.usage,web_search_calls:d.web_search_calls},null,2);details.append(summary,pre);div.append(details);history.push({role:'user',content:message},{role:'assistant',content:d.answer});status.textContent='Pesquisa concluída';}catch(err){turn(err.message,'assistant');status.textContent='Não foi possível concluir';}finally{send.disabled=false;prompt.focus();}});
|
||||
3
static/index.html
Normal file
3
static/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<!doctype html><html lang="pt-BR"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Grok · OCI Web Search</title><style>
|
||||
*{box-sizing:border-box}body{margin:0;background:#0b1020;color:#e9eef8;font:16px system-ui}main{max-width:900px;margin:auto;padding:32px 20px}header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #28324a;padding-bottom:24px}h1{font-size:26px;margin:0}small{color:#9baac4}.badge{border:1px solid #2c675c;color:#8ce5c7;border-radius:20px;padding:8px 12px;font-size:12px}#messages{min-height:50vh;padding-top:24px}.turn{padding:20px;background:#141d32;border:1px solid #27334d;border-radius:14px;margin:14px 0;white-space:pre-wrap;overflow-wrap:anywhere}.user{background:#1b2944;margin-left:12%}.meta{font-size:12px;color:#8ce5c7;margin-top:12px}a{color:#9bcaff}textarea{width:100%;background:#141d32;color:white;border:1px solid #354563;border-radius:12px;padding:16px;font:inherit;resize:vertical}button{background:#98e9d0;border:0;padding:12px 20px;border-radius:10px;font-weight:700;cursor:pointer}button:disabled{opacity:.5}.actions{display:flex;justify-content:space-between;margin-top:12px}#status{color:#adb9d0;font-size:13px}details{margin-top:12px;font-size:12px}pre{white-space:pre-wrap}footer{margin-top:24px;color:#8392ad;font-size:12px}
|
||||
</style><main><header><div><h1>Grok / Web Search</h1><small>Respostas atuais, fontes verificáveis · POC</small></div><span class="badge">OCI · grok-4.3</span></header><section id="messages"><div class="turn">O que você quer descobrir hoje? Pergunte sobre notícias ou fatos recentes. A busca web nativa está habilitada.</div></section><form id="form"><textarea id="prompt" rows="3" maxlength="8000" placeholder="Quais são as notícias de hoje? Cite fontes e datas." required></textarea><div class="actions"><span id="status">Pronto para pesquisar</span><button id="send">Enviar →</button></div></form><footer>Python + JavaScript · Inferência pela OCI · Histórico apenas nesta página. As respostas podem conter erros; confira as fontes.</footer></main><script src="./app.js"></script></html>
|
||||
34
validate.py
Normal file
34
validate.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Validação REST com uma chamada de inferência paga e verificações HTTP."""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
import requests
|
||||
|
||||
root = Path(__file__).resolve().parent
|
||||
url = os.environ.get("APP_BASE_URL", "http://localhost:8080/").rstrip("/") + "/"
|
||||
auth = ("poc", os.environ["APP_PASSWORD"])
|
||||
now = datetime.now(ZoneInfo("America/Sao_Paulo"))
|
||||
question = {"message": f"Pesquise uma notícia publicada hoje ({now.date()}, horário de Brasília). Informe título, data, resumo e URL. Verifique a página; se não encontrar, diga isso claramente."}
|
||||
started = time.monotonic()
|
||||
response = requests.post(url + "api/chat", auth=auth, json=question, timeout=180)
|
||||
report = {"tested_at": now.isoformat(), "request": question, "http": response.status_code,
|
||||
"seconds": round(time.monotonic() - started, 2), "response": response.json()}
|
||||
folder = root / "evidence"
|
||||
folder.mkdir(exist_ok=True)
|
||||
(folder / "rest-news-today.json").write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
assert data["answer"], "Resposta vazia"
|
||||
assert any(call["status"] == "completed" for call in data["web_search_calls"]), "Busca não confirmada"
|
||||
assert data["citations"], "Citações ausentes"
|
||||
checks = {path or "index": requests.get(url + path, auth=auth, timeout=20).status_code
|
||||
for path in ("", "app.js", "health")}
|
||||
checks["unauthenticated_post"] = requests.post(url + "api/chat", json=question, timeout=20).status_code
|
||||
checks["invalid_history"] = requests.post(url + "api/chat", auth=auth,
|
||||
json={"message": "test", "history": [{"role": "system", "content": "test"}]}, timeout=20).status_code
|
||||
(folder / "http-checks.json").write_text(json.dumps(checks, indent=2))
|
||||
assert checks == {"index": 200, "app.js": 200, "health": 200, "unauthenticated_post": 401, "invalid_history": 422}
|
||||
print(f"REST HTTP 200; busca e citações presentes; {report['seconds']} s. Evidências em ./evidence/")
|
||||
Reference in New Issue
Block a user