mirror of
https://github.com/mcloh/poc-grok-websearch.git
synced 2026-11-06 06:14:11 +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