PC SOFT

PROFESSIONAL NEWSGROUPS
WINDEVWEBDEV and WINDEV Mobile

Home → WINDEV 25 → Como instalar pelo Powershell a sua iA e usar ela com os seus sistemas
Como instalar pelo Powershell a sua iA e usar ela com os seus sistemas
Started by Boller, Jan., 29 2026 11:35 AM - 2 replies
Registered member
4,618 messages
Posted on January, 29 2026 - 11:35 AM
<#
Phoenix AI - Windows 11 Setup (Ollama + LLaMA + RAG + LangGraph)
- Instala dependências Python
- Cria venv
- Baixa modelos no Ollama
- Cria estrutura kb/
- Gera scripts index_kb.py e rag_graph.py
- Roda index e inicia o chat RAG

Como usar:
1) Instale manualmente: Python 3.11+ (marque "Add to PATH") e Ollama for Windows.
2) Abra PowerShell como Usuário normal.
3) Rode: .\setup_phoenix_ai.ps1

Obs: Se der erro de ExecutionPolicy:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
#>

$ErrorActionPreference = "Stop"

function Write-Step($msg) {
Write-Host "`n==> $msg" -ForegroundColor Cyan
}

function Assert-Command($cmd, $hint) {
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
Write-Host "`nERRO: '$cmd' não encontrado no PATH." -ForegroundColor Red
Write-Host "Dica: $hint" -ForegroundColor Yellow
exit 1
}
}

Write-Step "Checando pré-requisitos (python, pip, ollama)"
Assert-Command "python" "Instale Python 3.11+ (64-bit) e marque 'Add Python to PATH'."
Assert-Command "pip" "O pip vem junto do Python. Reinstale o Python marcando PATH."
Assert-Command "ollama" "Instale o Ollama for Windows e reabra o PowerShell."

Write-Step "Mostrando versões"
python --version
pip --version
ollama --version

# Pasta do projeto
$ProjectDir = Join-Path $PWD "phoenix_ai"
Write-Step "Criando pasta do projeto: $ProjectDir"
if (-not (Test-Path $ProjectDir)) {
New-Item -ItemType Directory -Path $ProjectDir | Out-Null
}
Set-Location $ProjectDir

# Criar venv
$VenvDir = Join-Path $ProjectDir ".venv"
Write-Step "Criando venv em: $VenvDir"
if (-not (Test-Path $VenvDir)) {
python -m venv .venv
} else {
Write-Host "Venv já existe. Mantendo." -ForegroundColor DarkGray
}

# Ativar venv
Write-Step "Ativando venv"
$ActivatePs1 = Join-Path $VenvDir "Scripts\Activate.ps1"
if (-not (Test-Path $ActivatePs1)) {
Write-Host "ERRO: Não achei $ActivatePs1" -ForegroundColor Red
exit 1
}
. $ActivatePs1

# Atualizar pip
Write-Step "Atualizando pip"
python -m pip install -U pip

# Instalar deps Python
Write-Step "Instalando dependências (langgraph, langchain, ollama, chroma)"
pip install -U langgraph langchain langchain-ollama langchain-chroma chromadb langchain-text-splitters

# Baixar modelos Ollama
Write-Step "Baixando modelos no Ollama (llama3.1 + embeddings)"
ollama pull llama3.1
ollama pull mxbai-embed-large

# Estrutura KB
Write-Step "Criando estrutura de base de conhecimento (kb/...)"
$KbDir = Join-Path $ProjectDir "kb"
$KbCommands = Join-Path $KbDir "commands"
$KbFunctions = Join-Path $KbDir "functions"
$KbCases = Join-Path $KbDir "cases"

foreach ($d in @($KbDir, $KbCommands, $KbFunctions, $KbCases)) {
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null }
}

# Criar um exemplo mínimo (para você testar na hora)
$SampleMd = Join-Path $KbCommands "print.md"
if (-not (Test-Path $SampleMd)) {
@"
# print

## O que faz
Imprime texto na saída padrão.

## Sintaxe
print(text)

## Parâmetros
- text: string

## Retorno
- void

## Exemplo
print("Olá, Phoenix!")
"@ | Set-Content -Path $SampleMd -Encoding utf8
}

# Gerar index_kb.py
Write-Step "Gerando index_kb.py"
$IndexPy = Join-Path $ProjectDir "index_kb.py"
@"
import os
from pathlib import Path

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

KB_DIR = Path("kb")
CHROMA_DIR = Path("chroma_db")
COLLECTION = "phoenix_kb"

def load_markdown_files(root: Path):
docs = []
for p in root.rglob("*.md"):
text = p.read_text(encoding="utf-8", errors="ignore")
rel = p.relative_to(root).as_posix()
docs.append({"text": text, "metadata": {"source": rel}})
return docs

def main():
if not KB_DIR.exists():
raise SystemExit("Pasta kb/ não existe. Crie kb/ e coloque .md lá dentro.")

embeddings = OllamaEmbeddings(model="mxbai-embed-large")

vectorstore = Chroma(
collection_name=COLLECTION,
persist_directory=str(CHROMA_DIR),
embedding_function=embeddings,
)

raw_docs = load_markdown_files(KB_DIR)

splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=150,
)

texts = []
metadatas = []
for d in raw_docs:
chunks = splitter.split_text(d["text"])
for i, c in enumerate(chunks):
texts.append(c)
metadatas.append({**d["metadata"], "chunk": i})

vectorstore.add_texts(texts=texts, metadatas=metadatas)

print(f"OK: indexados {len(texts)} chunks em {CHROMA_DIR}/ coleção {COLLECTION}")

if __name__ == "__main__":
main()
"@ | Set-Content -Path $IndexPy -Encoding utf8

# Gerar rag_graph.py
Write-Step "Gerando rag_graph.py"
$RagPy = Join-Path $ProjectDir "rag_graph.py"
@"
from typing_extensions import TypedDict, List

from langgraph.graph import StateGraph, START, END
from langchain_chroma import Chroma
from langchain_ollama import ChatOllama, OllamaEmbeddings

CHROMA_DIR = "chroma_db"
COLLECTION = "phoenix_kb"

class State(TypedDict):
question: str
contexts: List[str]
answer: str

def retrieve(state: State) -> dict:
embeddings = OllamaEmbeddings(model="mxbai-embed-large")
vs = Chroma(
collection_name=COLLECTION,
persist_directory=CHROMA_DIR,
embedding_function=embeddings,
)
retriever = vs.as_retriever(search_kwargs={"k": 6})
docs = retriever.get_relevant_documents(state["question"])
contexts = [d.page_content for d in docs]
return {"contexts": contexts}

def generate(state: State) -> dict:
llm = ChatOllama(model="llama3.1", temperature=0)

context_text = "\n\n---\n\n".join(state["contexts"]).strip()

prompt = f\"""
Você é o assistente oficial da linguagem Phoenix.
Responda APENAS usando o contexto abaixo. Se o contexto não tiver a resposta, diga:
"Não sei com base na base atual."

CONTEXTO:
{context_text}

PERGUNTA:
{state["question"]}

RESPOSTA (com exemplo de código e notas de uso):
\""".strip()

out = llm.invoke(prompt)
return {"answer": out.content}

def build_app():
g = StateGraph(State)
g.add_node("retrieve", retrieve)
g.add_node("generate", generate)

g.add_edge(START, "retrieve")
g.add_edge("retrieve", "generate")
g.add_edge("generate", END)

return g.compile()

if __name__ == "__main__":
app = build_app()

while True:
q = input("\nPergunta> ").strip()
if not q:
continue
if q.lower() in ("exit", "quit"):
break

result = app.invoke({"question": q, "contexts": [], "answer": ""})
print("\n" + result["answer"])
"@ | Set-Content -Path $RagPy -Encoding utf8

# Indexar KB
Write-Step "Indexando base de conhecimento (index_kb.py)"
python .\index_kb.py

# Rodar RAG
Write-Step "Iniciando chat RAG (rag_graph.py). Para sair: exit"
python .\rag_graph.py

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on January, 29 2026 - 11:46 AM
Sim — dá pra usar com WX (WinDev/WebDev/WinDev Mobile). Só não é “rodar Python dentro do WX”. O padrão é:

WX = app/IDE
IA = serviço local (Ollama + RAG) exposto via HTTP
Aí o WX chama esse serviço e recebe JSON.

a) Arquitetura que funciona
• Ollama rodando no Windows (local).
• Seu RAG + LangGraph vira uma API HTTP (FastAPI/Flask).
• No WX, você faz requisição HTTP (GET/POST) e renderiza resposta.

Vantagem: serve pra WinDev, WebDev e Mobile com o mesmo backend.



b) O que você reaproveita do que fizemos

Você reaproveita quase tudo:
• index_kb.py (cadastra a base no Chroma)
• lógica de retrieve+generate (RAG)
• só adiciona uma camada HTTP pra “perguntar” via endpoint.



c) Como o WX conversa com isso (na prática)

Você expõe um endpoint tipo:

POST http://127.0.0.1:8000/ask
Body:

{ "question": "Como funciona o comando print?" }

Resposta:

{
"answer": "...",
"sources": ["commands/print.md"]
}

No WX você chama HTTP e pronto.



d) Limites e pegadinhas
• WinDev Mobile: não dá pra depender de 127.0.0.1 no celular. Você precisa hospedar o serviço:
• na LAN (se for uso interno),
• num servidor Windows/Linux,
• ou embutir outro runtime (chato e geralmente não vale).
• Offline no celular: aí você entra em outro mundo (modelos pequenos on-device, bem mais complexo).
• Performance: LLaMA local pode ser pesado; pra desktop ok, pra servidor depende de GPU.



e) Recomendação “na lata”
• Pra WinDev/WebDev em PC/servidor: perfeito.
• Pra WinDev Mobile: funciona se a IA estiver num servidor acessível pelo app.



Pacote completo:
• API FastAPI (1 arquivo) com /ask e /reindex
• retornando answer + sources
• e o código WLanguage (WinDev/WebDev/Mobile) chamando esse endpoint (HTTPRequest/HTTPPost) com tratamento de erro e timeout.


Boa noite, Adriano. Sim — dá pra fazer “só com WinDev” (sem Python, sem LangGraph). O que você ainda precisa é de um motor LLM/embeddings rodando local, e o mais simples é o Ollama (é um serviço/exe externo). Aí o WinDev faz RAG + orquestração na unha.

A base técnica fica assim:
• WinDev: indexa seus docs, faz busca (RAG), monta prompt, chama o LLM.
• Ollama: entrega embeddings via POST /api/embed  e resposta via POST /api/chat .



a) O caminho recomendado “só WinDev”

a) HFSQL (ou outra base) guarda:
• source (nome do arquivo/comando)
• content (texto do doc)
• embedding_json (vetor em JSON)
• norm (norma do vetor, pra acelerar cosine)

b) WinDev indexa:
• lista arquivos com fListFile(..., frRecursive) 
• lê conteúdo com fLoadText() 
• gera embedding chamando Ollama POST /api/embed 
• grava no HFSQL.

c) Na pergunta:
• gera embedding da pergunta (Ollama embed)
• calcula similaridade (cosine) contra os docs
• pega Top-K trechos
• chama POST /api/chat com stream:false .



b) O que você cria no WinDev (HFSQL)

Crie um arquivo HFSQL (análise) chamado PHX_DOC com campos:
• doc_id (string 36) Unique
• source (string 260)
• content (Memo Text)
• embedding_json (Memo Text)
• norm (real)
• updated_at (DateTime)



c) Procedimentos WLanguage prontos (RAG sem Python)

c1) Embedding via Ollama (/api/embed)

(Endpoint atual recomendado é /api/embed; /api/embeddings está “superseded”. )

//##############################
// PhoenixAI_Embed
// Retorna: Variant com embeddings[1] (array de números)
// Ollama: POST http://localhost:11434/api/embed [oai_citation:7‡Ollama Docs](https://docs.ollama.com/api/embed)
//##############################
PROCEDURE PhoenixAI_Embed(sText is string, sEmbedModel is string = "mxbai-embed-large", sOllamaBaseURL is string = "http://127.0.0.1:11434")

IF sText = "" THEN RESULT Null END//IF

vReq is Variant
vReq.model = sEmbedModel
vReq.input = sText

sJSON is string = VariantToJSON(vReq) // [oai_citation:8‡doc.windev.com](https://doc.windev.com/en-US/…)

httpReq is httpRequest
httpReq.Method = httpPost
httpReq.URL = sOllamaBaseURL + "/api/embed"
httpReq.ContentType = "application/json"
httpReq.Content = sJSON

httpRep is httpResponse = HTTPSend(httpReq) // [oai_citation:9‡doc.windev.com](https://doc.windev.com/en-US/…)

IF ErrorOccurred THEN
Error(ErrorInfo(errFullDetails))
RESULT Null
END//IF

vRep is Variant = JSONToVariant(httpRep.Content) // [oai_citation:10‡doc.windev.com](https://doc.windev.com/en-US/…)

IF vRep.embeddings = Null THEN RESULT Null END//IF
IF vRep.embeddings..Occurrence < 1 THEN RESULT Null END//IF

// embeddings é number[][], vamos usar a 1ª linha
RESULT vRep.embeddings[1]

c2) Chat via Ollama (/api/chat) com stream:false

//##############################
// PhoenixAI_Chat
// Ollama: POST http://localhost:11434/api/chat [oai_citation:11‡Ollama Docs](https://docs.ollama.com/api/chat…)
//##############################
PROCEDURE PhoenixAI_Chat(sSystem is string, sUser is string, sModel is string = "llama3.1", sOllamaBaseURL is string = "http://127.0.0.1:11434")

vReq is Variant
vReq.model = sModel
vReq.stream = False

vReq.messages is Variant
vMsg1 is Variant
vMsg1.role = "system"
vMsg1.content = sSystem
vReq.messages.Add(vMsg1)

vMsg2 is Variant
vMsg2.role = "user"
vMsg2.content = sUser
vReq.messages.Add(vMsg2)

sJSON is string = VariantToJSON(vReq) // [oai_citation:12‡doc.windev.com](https://doc.windev.com/en-US/…)

httpReq is httpRequest
httpReq.Method = httpPost
httpReq.URL = sOllamaBaseURL + "/api/chat"
httpReq.ContentType = "application/json"
httpReq.Content = sJSON

httpRep is httpResponse = HTTPSend(httpReq) // [oai_citation:13‡doc.windev.com](https://doc.windev.com/en-US/…)

IF ErrorOccurred THEN
Error(ErrorInfo(errFullDetails))
RESULT ""
END//IF

vRep is Variant = JSONToVariant(httpRep.Content) // [oai_citation:14‡doc.windev.com](https://doc.windev.com/en-US/…)
IF vRep.message = Null THEN RESULT "" END//IF
IF vRep.message.content = Null THEN RESULT "" END//IF

RESULT vRep.message.content

c3) Cosine similarity (pra ranking)

//##############################
// PhoenixAI_VectorNorm
//##############################
PROCEDURE PhoenixAI_VectorNorm(vVec is Variant)

sum is real = 0

FOR i = 1 _TO_ vVec..Occurrence
val is real = vVec[i]
sum += val * val
END//FOR

RESULT Sqrt(sum)


//##############################
// PhoenixAI_Cosine
//##############################
PROCEDURE PhoenixAI_Cosine(vA is Variant, normA is real, vB is Variant, normB is real)

IF normA <= 0 OR normB <= 0 THEN RESULT 0 END//IF

dot is real = 0
maxI is int = vA..Occurrence
IF vB..Occurrence < maxI THEN maxI = vB..Occurrence END//IF

FOR i = 1 _TO_ maxI
dot += (vA[i] * vB[i])
END//FOR

RESULT dot / (normA * normB)

c4) Indexar KB (lendo .md e gravando no HFSQL)

//##############################
// PhoenixAI_IndexKB
// Varre kb\*.md (recursivo), lê texto, gera embedding e grava em PHX_DOC
// fListFile [oai_citation:15‡doc.windev.com](https://doc.windev.com/en-US/…) | fLoadText [oai_citation:16‡doc.windev.com](https://doc.windev.com/en-US/…)
//##############################
PROCEDURE PhoenixAI_IndexKB(sKbRoot is string, sOllamaBaseURL is string = "http://127.0.0.1:11434", sEmbedModel is string = "mxbai-embed-large")

IF sKbRoot = "" THEN RESULT False END//IF

sList is string = fListFile(sKbRoot + "\*.md", frRecursive) // [oai_citation:17‡doc.windev.com](https://doc.windev.com/en-US/…)
IF sList = "" THEN
Info("Nenhum .md encontrado em: " + sKbRoot)
RESULT False
END//IF

filePath is string

FOR EACH STRING filePath OF sList SEPARATED BY CR
sText is string = fLoadText(filePath) // [oai_citation:18‡doc.windev.com](https://doc.windev.com/en-US/…)
IF sText = "" THEN CONTINUE END//IF

vEmb is Variant = PhoenixAI_Embed(sText, sEmbedModel, sOllamaBaseURL)
IF vEmb = Null THEN CONTINUE END//IF

norm is real = PhoenixAI_VectorNorm(vEmb)

// Upsert simples por source (filePath)
PHX_DOC.source = filePath
HReadSeekFirst(PHX_DOC, source, filePath)
IF HFound() THEN
PHX_DOC.content = sText
PHX_DOC.embedding_json = VariantToJSON(vEmb) // [oai_citation:19‡doc.windev.com](https://doc.windev.com/en-US/…)
PHX_DOC.norm = norm
PHX_DOC.updated_at = DateTimeSys()
HModify(PHX_DOC)
ELSE
PHX_DOC.doc_id = GetUUID()
PHX_DOC.source = filePath
PHX_DOC.content = sText
PHX_DOC.embedding_json = VariantToJSON(vEmb)
PHX_DOC.norm = norm
PHX_DOC.updated_at = DateTimeSys()
HAdd(PHX_DOC)
END//IF
END//FOR

RESULT True

c5) Perguntar (RAG Top-K + Chat)

//##############################
// PhoenixAI_Ask
// Busca Top-K por cosine e chama Ollama /api/chat [oai_citation:20‡Ollama Docs](https://docs.ollama.com/api/chat…)
//##############################
PROCEDURE PhoenixAI_Ask(sQuestion is string, k is int = 6, sOllamaBaseURL is string = "http://127.0.0.1:11434", sLLMModel is string = "llama3.1", sEmbedModel is string = "mxbai-embed-large")

IF sQuestion = "" THEN RESULT "" END//IF

vQ is Variant = PhoenixAI_Embed(sQuestion, sEmbedModel, sOllamaBaseURL)
IF vQ = Null THEN RESULT "" END//IF
qNorm is real = PhoenixAI_VectorNorm(vQ)

bestScore is array of real
bestText is array of string
bestSource is array of string

// inicializa arrays com tamanho k
FOR i = 1 _TO_ k
bestScore[i] = -1
bestText[i] = ""
bestSource[i] = ""
END//FOR

HReadFirst(PHX_DOC)
WHILE HFound()
IF PHX_DOC.embedding_json <> "" AND PHX_DOC.norm > 0 THEN
vD is Variant = JSONToVariant(PHX_DOC.embedding_json) // [oai_citation:21‡doc.windev.com](https://doc.windev.com/en-US/…)
score is real = PhoenixAI_Cosine(vQ, qNorm, vD, PHX_DOC.norm)

// inserção simples no top-k
FOR pos = 1 _TO_ k
IF score > bestScore[pos] THEN
// shift pra baixo
FOR j = k DOWNTO pos + 1
bestScore[j] = bestScore[j-1]
bestText[j] = bestText[j-1]
bestSource[j] = bestSource[j-1]
END//FOR
bestScore[pos] = score
bestText[pos] = PHX_DOC.content
bestSource[pos] = PHX_DOC.source
BREAK
END//IF
END//FOR
END//IF

HReadNext(PHX_DOC)
END//WHILE

ctx is string = ""
FOR i = 1 _TO_ k
IF bestText[i] <> "" THEN
IF ctx <> "" THEN ctx += CR + "-----" + CR END//IF
ctx += bestText[i]
END//IF
END//FOR

sSystem is string = "Você é o assistente oficial da linguagem Phoenix. Responda só com base no CONTEXTO. Se não tiver no contexto, diga: 'Não sei com base na base atual.'"
sUser is string = "CONTEXTO:" + CR + ctx + CR + CR + "PERGUNTA:" + CR + sQuestion

RESULT PhoenixAI_Chat(sSystem, sUser, sLLMModel, sOllamaBaseURL)




d) O que isso substitui do “LangGraph”

O LangGraph é só orquestração em grafo. No “só WinDev”, você faz isso com:
• procedures (IndexKB, Embed, Retrieve, Ask)
• regras (“se não tem contexto bom, responda ‘não sei’”)
• logs/Trace/erros.

Funciona igual, só menos “framework”.



e) Ponto crítico: endereço do Ollama

Por padrão o Ollama sobe em porta 11434 (localhost) e expõe endpoints tipo /api/embed e /api/chat. 
Se seu WinDev e Ollama estiverem na mesma máquina: http://127.0.0.1:11434.



Se você rodar isso com poucos milhares de docs, HFSQL + cosine dá conta. Se crescer muito, a evolução natural é trocar o “retrieve” por um banco vetorial de verdade — mas aí já foge do “só WinDev”.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,618 messages
Posted on January, 29 2026 - 12:24 PM
Sim. WebDev + Ollama é ainda mais fácil que WinDev, porque você já está no mundo HTTP.

Você tem 2 jeitos:

a) Jeito certo (recomendado): WebDev chama um “AI Gateway” (WinDev/Python/Node)
• WebDev (site) faz POST pra um endpoint seu /api/ai/ask
• Esse endpoint chama o Ollama local/servidor e aplica RAG
• Volta JSON pro WebDev renderizar

Por quê: WebDev rodando no browser não pode acessar localhost:11434 do servidor do usuário; e você quer controle, logs, segurança.

b) Jeito rápido (só se Ollama estiver no MESMO servidor do WebDev)

Se seu WebDev está hospedado no mesmo Windows/Linux que roda o Ollama:
• Você faz o WebDev server-side chamar http://127.0.0.1:11434/api/chat direto.



c) Código WebDev (server-side) chamando Ollama direto

c1) Função: chat (sem streaming)

//##############################
// WEB_PhoenixAI_Chat (server-side)
//##############################
PROCEDURE WEB_PhoenixAI_Chat(sSystem is string, sUser is string, sModel is string = "llama3.1", sOllamaBaseURL is string = "http://127.0.0.1:11434")

vReq is Variant
vReq.model = sModel
vReq.stream = False

vReq.messages is Variant
vMsg1 is Variant
vMsg1.role = "system"
vMsg1.content = sSystem
vReq.messages.Add(vMsg1)

vMsg2 is Variant
vMsg2.role = "user"
vMsg2.content = sUser
vReq.messages.Add(vMsg2)

sJSON is string = VariantToJSON(vReq)

httpReq is httpRequest
httpReq.Method = httpPost
httpReq.URL = sOllamaBaseURL + "/api/chat"
httpReq.ContentType = "application/json"
httpReq.Content = sJSON

httpRep is httpResponse = HTTPSend(httpReq)

IF ErrorOccurred THEN
Trace("Ollama error: " + ErrorInfo(errFullDetails))
RESULT ""
END//IF

vRep is Variant = JSONToVariant(httpRep.Content)
IF vRep.message = Null OR vRep.message.content = Null THEN RESULT "" END//IF

RESULT vRep.message.content

c2) Endpoint WebDev (Page/PROC Webservice): /api/ai/ask

Você cria um procedimento server-side que:
• lê RequestContent()
• parseia JSON (JSONToVariant)
• chama WEB_PhoenixAI_Chat
• devolve JSON (ResponseContentType("application/json") + ResponseWrite())

Exemplo:

//##############################
// WEB_API_AI_Ask (server-side)
// Body: {"question":"..."} -> {"answer":"..."}
//##############################
PROCEDURE WEB_API_AI_Ask()

ResponseContentType("application/json")

sBody is string = RequestContent()
IF sBody = "" THEN
ResponseWrite("{""error"":""body vazio""}")
RETURN
END//IF

vIn is Variant = JSONToVariant(sBody)
IF vIn.question = Null THEN
ResponseWrite("{""error"":""campo question ausente""}")
RETURN
END//IF

sQuestion is string = vIn.question

sSystem is string = "Você é o assistente oficial da Phoenix. Responda só com base no contexto fornecido."
// Aqui você encaixa o RAG (buscar docs) e monta CONTEXTO.
// Pra versão mínima sem RAG:
sUser is string = sQuestion

sAnswer is string = WEB_PhoenixAI_Chat(sSystem, sUser)

vOut is Variant
vOut.answer = sAnswer
ResponseWrite(VariantToJSON(vOut))




d) Onde entra o RAG no WebDev

Exatamente igual no WinDev:
• você guarda docs + embeddings no banco (HFSQL ou Postgres)
• faz retrieve top-k
• monta CONTEXTO
• chama WEB_PhoenixAI_Chat

WebDev só vira “o servidor” que faz o fluxo.



e) Regra de ouro (pra não dar dor de cabeça)
• Se o Ollama estiver em outro servidor, WebDev chama via IP/hostname.
• Não exponha o Ollama direto pra internet (sem proxy/autenticação). Faça gateway.



Se você quiser, eu te solto o pacote “completo WebDev” com:
• tabela HFSQL PHX_DOC
• WEB_IndexKB() (reindex)
• WEB_RetrieveTopK() (cosine)
• endpoint /api/ai/ask retornando answer + sources (pra cortar alucinação).

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/