diff --git a/.env.example b/.env.example index 4349089..835beeb 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ OCI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1 +OCI_MODEL=xai.grok-4.3 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 diff --git a/.gitignore b/.gitignore index b93aa5e..a769de5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ render_check.py evidence/* !evidence/RELATORIO.md !evidence/validation-summary.json +!evidence/grok-websearch-models.json diff --git a/README.md b/README.md index 3bdd0d1..d2c8a1b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. +POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa Grok (padrão `xai.grok-4.3`, configurável por `OCI_MODEL`) pela OCI Generative AI e habilita a ferramenta nativa `web_search` na Responses API. A aplicação chama exclusivamente o endpoint OCI. ## Estrutura @@ -11,14 +11,17 @@ POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa `xai.grok- │ ├── index.html # Interface e estilos │ └── app.js # Chat, histórico e citações ├── requirements.txt +├── requirements-test.txt # TestClient para matriz de modelos ├── Dockerfile ├── .dockerignore ├── .gitignore ├── .env.example # Variáveis sem credenciais ├── validate.py # Validação REST com inferência real +├── test_models.py # Matriz de modelos pela mesma aplicação └── evidence/ ├── RELATORIO.md # Resultados da POC - └── validation-summary.json # Evidências selecionadas + ├── validation-summary.json # Evidências selecionadas + └── grok-websearch-models.json # Matriz de compatibilidade datada ``` ## Configuração @@ -37,6 +40,7 @@ Preencha `.env`: | Variável | Finalidade | |---|---| | `OCI_BASE_URL` | `https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1` | +| `OCI_MODEL` | Modelo Grok; padrão `xai.grok-4.3` | | `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` | @@ -92,6 +96,49 @@ A Oracle informa que os modelos Grok acessados pela OCI são hospedados externam 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`. +## Modelos com web_search confirmado em 14/10/2026 + +Teste da mesma rota `/api/chat` em processos locais isolados via FastAPI TestClient, com inferência real na OCI **us-chicago-1**, endpoint **`/v1/responses`**, projeto ativo e `tools: [{"type":"web_search"}]`. Não foram chamadas APIs diretas da xAI. A implantação pública original foi validada com Grok 4.3; esta matriz usa a aplicação local para variar os modelos sem interromper a VM. + +| Modelo solicitado e retornado | Busca nativa | HTTP app/OCI | Buscas concluídas | Citações | Tempo (s) | +|---|---|---|---:|---:|---:| +| `xai.grok-4.20-0309-non-reasoning` | Confirmado | 200/200 | 2 | 1 | 3.62 | +| `xai.grok-4.20-0309-reasoning` | Confirmado | 200/200 | 1 | 2 | 14.80 | +| `xai.grok-4.20-non-reasoning` | Confirmado | 200/200 | 2 | 1 | 3.22 | +| `xai.grok-4.20-reasoning` | Confirmado | 200/200 | 2 | 1 | 17.09 | +| `xai.grok-4.3` | Confirmado | 200/200 | 2 | 1 | 9.24 | +| `xai.grok-4.6` | Confirmado | 200/200 | 1 | 1 | 35.17 | +| `xai.grok-4.7` | Confirmado | 200/200 | 3 | 1 | 12.97 | + +Critério de confirmação: HTTP 200, pelo menos um evento `web_search_call` com ação `search` e status `completed`, fontes não vazias e citações estruturadas. HTTP 200 isolado não comprova a execução de busca. + +Foram sete chamadas, sem retries, com até duas inferências simultâneas e 2400 tokens de saída por chamada. Todos os modelos retornaram exatamente o identificador solicitado. Os testes cobrem os sete modelos de texto Grok do catálogo local; Voice Agent, Multi-Agent e outros identificadores não foram testados. O resultado vale para esta data, região, endpoint e permissões; não confirma todas as regiões ou interfaces OCI. A validação da busca não equivale à verificação independente de todas as notícias retornadas. + +Os tempos são observações de uma chamada por modelo, sob concorrência limitada, e não constituem benchmark. Consumo agregado: **76.374 tokens** (69.948 de entrada e 6.426 de saída), com **13 ferramentas** contabilizadas pelo serviço. `cost_in_usd_ticks` foi preservado sem conversão financeira. + +Para escolher um modelo, defina `OCI_MODEL` no `.env` e reinicie o serviço. O padrão continua `xai.grok-4.3`; `/health` e o indicador da UI refletem a configuração. + +Para reproduzir a matriz (sete chamadas pagas): + +```bash +. .venv/bin/activate +pip install -r requirements-test.txt +set -a +. ./.env +set +a +python test_models.py +``` + +Para testar somente um modelo: + +```bash +python test_models.py --models xai.grok-4.7 +``` + +O [script](test_models.py) salva respostas brutas localmente em `./evidence/model-tests-raw/` (ignoradas pelo Git) e um [resumo da matriz](evidence/grok-websearch-models.json), sem credenciais nem identificadores de projeto. Execuções parciais na mesma data preservam os resultados dos demais modelos já testados. As versões do runtime usado na matriz original estão no JSON; a reprodução instala as dependências fixadas do aplicativo. + +Referências do fornecedor: [Grok 4.20](https://docs.oracle.com/en-us/iaas/Content/generative-ai/xai-grok-4-20.htm), [Grok 4.6](https://docs.oracle.com/en-us/iaas/Content/generative-ai/xai-grok-4-6.htm) e [Grok 4.7](https://docs.oracle.com/en-us/iaas/Content/generative-ai/x-ai-grok-4-7.htm). A confirmação nesta tabela vem dos testes de execução. + ## API REST `POST /api/chat` com HTTP Basic. Na publicação com prefixo, use `POST /grok-poc/api/chat`. diff --git a/app.py b/app.py index 5932479..949f14a 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ 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'} +def health():return {'status':'ok','provider':'OCI','model':os.environ.get('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)]) @@ -32,7 +32,7 @@ async def chat(q:Chat): 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}], + body={'model':os.environ.get('OCI_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() diff --git a/evidence/RELATORIO.md b/evidence/RELATORIO.md index 1e1fc31..4fbb9ff 100644 --- a/evidence/RELATORIO.md +++ b/evidence/RELATORIO.md @@ -19,3 +19,7 @@ Fonte: [Agência Brasil — Confira a agenda dos presidenciáveis nesta quarta-f 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. + +## Matriz de versões Grok + +Em 14/10/2026, sete modelos de texto passaram no teste da mesma aplicação via TestClient e inferência OCI real: Grok 4.3, 4.6, 4.7 e as quatro variantes 4.20. Veja a [matriz estruturada](grok-websearch-models.json) e a [metodologia no README](../README.md#modelos-com-web_search-confirmado-em-14102026). Cada confirmação exige busca concluída com fontes e citações; os tempos são observações isoladas. A matriz consumiu 76.374 tokens e 13 ferramentas contabilizadas pelo serviço, separadamente das validações originais acima. diff --git a/evidence/grok-websearch-models.json b/evidence/grok-websearch-models.json new file mode 100644 index 0000000..ae3d7bb --- /dev/null +++ b/evidence/grok-websearch-models.json @@ -0,0 +1,414 @@ +{ + "tested_at": "2026-10-14T11:27:10.525049-03:00", + "region": "us-chicago-1", + "endpoint": "/v1/responses", + "method": "POST /api/chat via FastAPI TestClient; inferência OCI real; um processo isolado por modelo", + "max_output_tokens": 2400, + "concurrency": 2, + "retries": 0, + "confirmation_criteria": "HTTP 200, search completed, fontes não vazias e citações estruturadas", + "results": [ + { + "requested_model": "xai.grok-4.20-0309-non-reasoning", + "returned_model": "xai.grok-4.20-0309-non-reasoning", + "tested_at": "2026-10-14T11:26:10.793751-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 3.62, + "websearch_confirmed": true, + "completed_searches": 2, + "source_count": 15, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 10036, + "input_tokens_details": { + "cached_tokens": 1024 + }, + "output_tokens": 196, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 10232, + "num_sources_used": 0, + "num_server_side_tools_used": 2, + "cost_in_usd_ticks": 219598000, + "server_side_tool_usage_details": { + "web_search_calls": 2, + "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": 9058, + "output_tokens": 138 + } + }, + "error": null, + "health_model": "xai.grok-4.20-0309-non-reasoning", + "citation_urls": [ + "https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/" + ] + }, + { + "requested_model": "xai.grok-4.20-0309-reasoning", + "returned_model": "xai.grok-4.20-0309-reasoning", + "tested_at": "2026-10-14T11:26:10.782598-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 14.8, + "websearch_confirmed": true, + "completed_searches": 1, + "source_count": 10, + "citation_count": 2, + "tool_events": [ + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 7712, + "input_tokens_details": { + "cached_tokens": 256 + }, + "output_tokens": 1491, + "output_tokens_details": { + "reasoning_tokens": 1375 + }, + "total_tokens": 9203, + "num_sources_used": 0, + "num_server_side_tools_used": 1, + "cost_in_usd_ticks": 180987000, + "server_side_tool_usage_details": { + "web_search_calls": 1, + "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": 6732, + "output_tokens": 864 + } + }, + "error": null, + "health_model": "xai.grok-4.20-0309-reasoning", + "citation_urls": [ + "https://correiodobrasil.com.br/c/ultimas-noticias/" + ] + }, + { + "requested_model": "xai.grok-4.20-non-reasoning", + "returned_model": "xai.grok-4.20-non-reasoning", + "tested_at": "2026-10-14T11:26:14.931513-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 3.22, + "websearch_confirmed": true, + "completed_searches": 2, + "source_count": 15, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 10944, + "input_tokens_details": { + "cached_tokens": 1024 + }, + "output_tokens": 224, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 11168, + "num_sources_used": 0, + "num_server_side_tools_used": 2, + "cost_in_usd_ticks": 231648000, + "server_side_tool_usage_details": { + "web_search_calls": 2, + "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": 9966, + "output_tokens": 153 + } + }, + "error": null, + "health_model": "xai.grok-4.20-non-reasoning", + "citation_urls": [ + "https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/" + ] + }, + { + "requested_model": "xai.grok-4.20-reasoning", + "returned_model": "xai.grok-4.20-reasoning", + "tested_at": "2026-10-14T11:26:18.687154-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 17.09, + "websearch_confirmed": true, + "completed_searches": 2, + "source_count": 10, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 7882, + "input_tokens_details": { + "cached_tokens": 1024 + }, + "output_tokens": 1531, + "output_tokens_details": { + "reasoning_tokens": 1329 + }, + "total_tokens": 9413, + "num_sources_used": 0, + "num_server_side_tools_used": 2, + "cost_in_usd_ticks": 226048000, + "server_side_tool_usage_details": { + "web_search_calls": 2, + "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": 6902, + "output_tokens": 732 + } + }, + "error": null, + "health_model": "xai.grok-4.20-reasoning", + "citation_urls": [ + "https://paraibaonline.com.br/brasil/2026/10/14/o-que-tem-nas-capas-dos-jornais-do-brasil-nesta-quarta-feira-14-10/" + ] + }, + { + "requested_model": "xai.grok-4.3", + "returned_model": "xai.grok-4.3", + "tested_at": "2026-10-14T11:26:26.105038-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 9.24, + "websearch_confirmed": true, + "completed_searches": 2, + "source_count": 10, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 8092, + "input_tokens_details": { + "cached_tokens": 1280 + }, + "output_tokens": 898, + "output_tokens_details": { + "reasoning_tokens": 734 + }, + "total_tokens": 8990, + "num_sources_used": 0, + "num_server_side_tools_used": 2, + "cost_in_usd_ticks": 210160000, + "server_side_tool_usage_details": { + "web_search_calls": 2, + "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": 6926, + "output_tokens": 456 + } + }, + "error": null, + "health_model": "xai.grok-4.3", + "citation_urls": [ + "https://www.cnnbrasil.com.br/nacional/brasil/temporais-e-chuva-forte-veja-como-fica-o-tempo-no-brasil-nesta-quarta-14/" + ] + }, + { + "requested_model": "xai.grok-4.6", + "returned_model": "xai.grok-4.6", + "tested_at": "2026-10-14T11:26:35.843431-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 35.17, + "websearch_confirmed": true, + "completed_searches": 1, + "source_count": 10, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 8712, + "input_tokens_details": { + "cached_tokens": 1280 + }, + "output_tokens": 878, + "output_tokens_details": { + "reasoning_tokens": 732 + }, + "total_tokens": 9590, + "num_sources_used": 0, + "num_server_side_tools_used": 1, + "cost_in_usd_ticks": 257720000, + "server_side_tool_usage_details": { + "web_search_calls": 1, + "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": 7510, + "output_tokens": 535 + } + }, + "error": null, + "health_model": "xai.grok-4.6", + "citation_urls": [ + "https://www.cnnbrasil.com.br/nacional/brasil/temporais-e-chuva-forte-veja-como-fica-o-tempo-no-brasil-nesta-quarta-14/" + ] + }, + { + "requested_model": "xai.grok-4.7", + "returned_model": "xai.grok-4.7", + "tested_at": "2026-10-14T11:24:43.909978-03:00", + "app_http": 200, + "oci_http": 200, + "seconds": 12.97, + "websearch_confirmed": true, + "completed_searches": 3, + "source_count": 20, + "citation_count": 1, + "tool_events": [ + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + }, + { + "status": "completed", + "action": "search" + } + ], + "usage": { + "input_tokens": 16570, + "input_tokens_details": { + "cached_tokens": 3456 + }, + "output_tokens": 1208, + "output_tokens_details": { + "reasoning_tokens": 988 + }, + "total_tokens": 17778, + "num_sources_used": 0, + "num_server_side_tools_used": 3, + "cost_in_usd_ticks": 502040000, + "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": 14183, + "output_tokens": 1022 + } + }, + "error": null, + "health_model": "xai.grok-4.7", + "citation_urls": [ + "https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/" + ] + } + ], + "aggregate_usage": { + "input_tokens": 69948, + "output_tokens": 6426, + "total_tokens": 76374, + "num_server_side_tools_used": 13, + "cost_in_usd_ticks": 1828201000 + }, + "test_runtime": { + "python": "3.12.7", + "fastapi": "0.143.1", + "starlette": "1.8.0", + "httpx": "0.28.1", + "requests": "2.34.2" + } +} diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..f3d6397 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +httpx==0.28.1 diff --git a/static/app.js b/static/app.js index 5a1585b..b387137 100644 --- a/static/app.js +++ b/static/app.js @@ -2,3 +2,5 @@ const form=document.querySelector('#form'),box=document.querySelector('#messages 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();}}); + +fetch('./health').then(r=>r.json()).then(d=>{document.querySelector('.badge').textContent='OCI · '+d.model.replace('xai.','');}).catch(()=>{}); diff --git a/test_models.py b/test_models.py new file mode 100644 index 0000000..13516d1 --- /dev/null +++ b/test_models.py @@ -0,0 +1,112 @@ +"""Teste pago da mesma aplicação ASGI com modelos Grok via OCI. + +Executar com variáveis de .env exportadas. Concorrência máxima: 2. +Uma chamada por modelo, sem retries; até 2400 tokens de saída e timeout de 150s. +""" +import argparse +import concurrent.futures +import importlib.metadata +from datetime import datetime +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import time +from zoneinfo import ZoneInfo + +MODELS = [ + 'xai.grok-4.20-0309-non-reasoning', 'xai.grok-4.20-0309-reasoning', + 'xai.grok-4.20-non-reasoning', 'xai.grok-4.20-reasoning', + 'xai.grok-4.3', 'xai.grok-4.6', 'xai.grok-4.7', +] +ROOT = Path(__file__).resolve().parent + +def worker(model): + os.environ['OCI_MODEL'] = model + from fastapi.testclient import TestClient + import app + captured = {} + original = app.requests.post + def observe(*args, **kwargs): + response = original(*args, **kwargs) + captured['http'] = response.status_code + captured['body'] = response.json() + return response + app.requests.post = observe + now = datetime.now(ZoneInfo('America/Sao_Paulo')) + message = (f'Use web_search para encontrar uma notícia publicada hoje ({now.date()}). ' + 'Faça uma consulta curta e cite uma fonte com URL e data. Responda em até 100 palavras. ' + 'Se não encontrar notícia de hoje, declare isso.') + started = time.monotonic() + with TestClient(app.app) as client: + health = client.get('/health').json() + response = client.post('/api/chat', auth=('poc', os.environ['APP_PASSWORD']), json={'message': message}) + data = response.json() + calls = data.get('web_search_calls', []) + completed = [c for c in calls if c.get('status') == 'completed'] + # Exigir ação real de pesquisa, fontes e citações: HTTP 200 sozinho não basta. + searches = [c for c in completed if c.get('action', {}).get('type') == 'search'] + sources = [s for c in searches for s in c.get('action', {}).get('sources', [])] + confirmed = response.status_code == 200 and bool(searches) and bool(sources) and bool(data.get('citations')) + body = captured.get('body', {}) + error = body.get('error', {}) + error_text = str(error.get('message', '')) + for key in ('OCI_API_KEY', 'OCI_PROJECT_ID', 'APP_PASSWORD'): + value = os.environ.get(key) + if value: error_text = error_text.replace(value, '[redacted]') + error_text = re.sub(r'ocid1\.[\w.-]+', '[redacted]', error_text) + row = {'requested_model': model, 'returned_model': data.get('model'), 'tested_at': now.isoformat(), + 'app_http': response.status_code, 'oci_http': captured.get('http'), + 'seconds': round(time.monotonic()-started, 2), 'websearch_confirmed': confirmed, + 'completed_searches': len(searches), 'source_count': len(sources), + 'citation_count': len(data.get('citations', [])), + 'tool_events': [{'status': c.get('status'), 'action': c.get('action', {}).get('type')} for c in calls], + 'usage': data.get('usage'), + 'citation_urls': sorted({c['url'] for c in data.get('citations', []) if c.get('url')}), + 'error': error_text or data.get('detail'), 'health_model': health['model']} + folder = ROOT/'evidence'/'model-tests-raw' + folder.mkdir(parents=True, exist_ok=True) + (folder/(model+'.json')).write_text(json.dumps({'message': message, 'app_response': data, 'upstream': captured}, ensure_ascii=False, indent=2)) + print(json.dumps(row, ensure_ascii=False)) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--worker', choices=MODELS) + parser.add_argument('--models', nargs='+', choices=MODELS, default=MODELS) + args = parser.parse_args() + if args.worker: + worker(args.worker) + return + def run(model): + result = subprocess.run([sys.executable, str(Path(__file__).resolve()), '--worker', model], + capture_output=True, text=True, timeout=180, cwd=ROOT) + if result.returncode: + raise RuntimeError(f'{model}: worker failed (exit {result.returncode})') + row = json.loads(result.stdout) + print(f"{model}: app={row['app_http']} OCI={row['oci_http']} search={row['websearch_confirmed']} {row['seconds']}s", flush=True) + return row + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + rows = list(pool.map(run, args.models)) + previous = ROOT/'evidence/grok-websearch-models.json' + if previous.exists(): + old = json.loads(previous.read_text()) + today = datetime.now(ZoneInfo('America/Sao_Paulo')).date().isoformat() + if old.get('tested_at', '').startswith(today): + rows += [row for row in old['results'] if row['requested_model'] not in args.models] + rows.sort(key=lambda row: MODELS.index(row['requested_model'])) + report = {'tested_at': datetime.now(ZoneInfo('America/Sao_Paulo')).isoformat(), + 'region': 'us-chicago-1', 'endpoint': '/v1/responses', + 'method': 'POST /api/chat via FastAPI TestClient; inferência OCI real; um processo isolado por modelo', + 'test_runtime': {'python': sys.version.split()[0], **{name: importlib.metadata.version(name) for name in ['fastapi', 'starlette', 'httpx', 'requests']}}, + 'max_output_tokens': 2400, 'concurrency': 2, 'retries': 0, + 'confirmation_criteria': 'HTTP 200, search completed, fontes não vazias e citações estruturadas', + 'results': rows, + 'aggregate_usage': {key: sum((r.get('usage') or {}).get(key, 0) or 0 for r in rows) + for key in ['input_tokens', 'output_tokens', 'total_tokens', 'num_server_side_tools_used', 'cost_in_usd_ticks']}} + (ROOT/'evidence').mkdir(exist_ok=True) + (ROOT/'evidence/grok-websearch-models.json').write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n') + +if __name__ == '__main__': + main()