Skip to content

Commit 0b80a8e

Browse files
DMontgomery40claude
andcommitted
ci: complete auto-sync workflow for ragweld.com demo
- Workflow now checks out both repos and syncs web/ to vendor/demo/ - Preserves MSW mocks during sync - Patches vite.config.ts and main.tsx for /demo/ base - Commits and pushes to ragweld.com (triggers Netlify) Also fixes mypy type errors: - server/chat/handler.py: Add FusionProtocol type annotations - server/api/docker.py: Convert dicts to DockerContainer models Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7a65b25 commit 0b80a8e

3 files changed

Lines changed: 260 additions & 16 deletions

File tree

.github/workflows/sync-ragweld.yml

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,105 @@
1-
# Trigger ragweld.com rebuild when web/ changes
1+
# Sync tribrid-rag/web to ragweld.com/vendor/demo when web/ changes
22
# This ensures the live demo stays in sync with the source code
33
#
44
# Setup required:
5-
# 1. In Netlify: Site settings > Build & deploy > Build hooks > Add build hook
6-
# Name: "sync-from-tribrid-rag"
7-
# 2. In GitHub: Settings > Secrets > Add secret
8-
# Name: RAGWELD_NETLIFY_HOOK
9-
# Value: The build hook URL from step 1
5+
# 1. Create a GitHub Personal Access Token (PAT) with repo scope
6+
# 2. Add it as a secret: RAGWELD_DEPLOY_TOKEN
7+
# Settings > Secrets > Actions > New repository secret
108

11-
name: Sync ragweld.com
9+
name: Sync ragweld.com Demo
1210

1311
on:
1412
push:
1513
branches: [main]
1614
paths:
1715
- 'web/**'
1816
- '.github/workflows/sync-ragweld.yml'
17+
workflow_dispatch: # Allow manual trigger
1918

2019
jobs:
21-
trigger-rebuild:
20+
sync-demo:
2221
runs-on: ubuntu-latest
2322
steps:
24-
- name: Trigger Netlify build
23+
- name: Checkout tribrid-rag
24+
uses: actions/checkout@v4
25+
with:
26+
path: tribrid-rag
27+
28+
- name: Checkout ragweld.com
29+
uses: actions/checkout@v4
30+
with:
31+
repository: DMontgomery40/ragweld.com
32+
token: ${{ secrets.RAGWELD_DEPLOY_TOKEN }}
33+
path: ragweld.com
34+
35+
- name: Sync web/ to vendor/demo/
36+
run: |
37+
# Backup MSW mocks (ragweld-specific)
38+
if [ -d "ragweld.com/vendor/demo/src/mocks" ]; then
39+
cp -R ragweld.com/vendor/demo/src/mocks /tmp/ragweld-mocks-backup
40+
fi
41+
42+
# Remove old demo source (keep node_modules if exists)
43+
cd ragweld.com/vendor/demo
44+
find . -maxdepth 1 ! -name 'node_modules' ! -name '.' -exec rm -rf {} +
45+
cd ../../..
46+
47+
# Copy fresh web/ source
48+
cp -R tribrid-rag/web/* ragweld.com/vendor/demo/
49+
50+
# Remove build artifacts and node_modules from copy
51+
rm -rf ragweld.com/vendor/demo/node_modules
52+
rm -rf ragweld.com/vendor/demo/dist
53+
rm -rf ragweld.com/vendor/demo/.tests
54+
55+
# Restore MSW mocks
56+
if [ -d "/tmp/ragweld-mocks-backup" ]; then
57+
rm -rf ragweld.com/vendor/demo/src/mocks
58+
cp -R /tmp/ragweld-mocks-backup ragweld.com/vendor/demo/src/mocks
59+
fi
60+
61+
# Patch vite.config.ts: base /web/ -> /demo/
62+
sed -i "s|base: '/web/'|base: '/demo/'|g" ragweld.com/vendor/demo/vite.config.ts
63+
64+
# Patch main.tsx: basename /web -> /demo
65+
sed -i 's|basename="/web"|basename="/demo"|g' ragweld.com/vendor/demo/src/main.tsx
66+
67+
# Ensure MSW init code is in main.tsx (add if missing)
68+
if ! grep -q "enableMocking" ragweld.com/vendor/demo/src/main.tsx; then
69+
echo "Warning: MSW init code missing from main.tsx - manual fix required"
70+
fi
71+
72+
- name: Check for changes
73+
id: changes
74+
run: |
75+
cd ragweld.com
76+
git add -A
77+
if git diff --staged --quiet; then
78+
echo "No changes to commit"
79+
echo "has_changes=false" >> $GITHUB_OUTPUT
80+
else
81+
echo "Changes detected"
82+
echo "has_changes=true" >> $GITHUB_OUTPUT
83+
fi
84+
85+
- name: Commit and push
86+
if: steps.changes.outputs.has_changes == 'true'
87+
run: |
88+
cd ragweld.com
89+
git config user.name "github-actions[bot]"
90+
git config user.email "github-actions[bot]@users.noreply.github.com"
91+
git commit -m "chore: sync demo from tribrid-rag/web
92+
93+
Auto-synced from tribrid-rag commit: ${{ github.sha }}
94+
95+
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
96+
git push
97+
98+
- name: Summary
2599
run: |
26-
if [ -z "${{ secrets.RAGWELD_NETLIFY_HOOK }}" ]; then
27-
echo "Warning: RAGWELD_NETLIFY_HOOK secret not set"
28-
echo "Skipping Netlify trigger"
29-
exit 0
100+
if [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then
101+
echo "✅ Demo synced and pushed to ragweld.com"
102+
echo "Netlify will auto-deploy from the push"
103+
else
104+
echo "ℹ️ No changes detected - demo already in sync"
30105
fi
31-
curl -X POST -d '{}' "${{ secrets.RAGWELD_NETLIFY_HOOK }}"
32-
echo "Triggered ragweld.com rebuild"

server/api/docker.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from server.models.tribrid_config_model import (
1919
DevStackRestartResponse,
2020
DevStackStatusResponse,
21+
DockerContainer,
2122
DockerContainersResponse,
2223
DockerStatus,
2324
LokiStatus,
@@ -442,7 +443,8 @@ async def list_docker_containers() -> DockerContainersResponse:
442443
cfg = TriBridConfig()
443444
timeout = 10
444445
env = _docker_env(cfg)
445-
containers = await _list_docker_containers(timeout_s=timeout, env=env)
446+
container_dicts = await _list_docker_containers(timeout_s=timeout, env=env)
447+
containers = [DockerContainer.model_validate(c) for c in container_dicts]
446448
return DockerContainersResponse(containers=containers)
447449

448450

server/chat/handler.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import time
5+
from collections.abc import AsyncIterator
6+
7+
from server.chat.generation import generate_chat_text, stream_chat_text
8+
from server.chat.provider_router import select_provider_route
9+
from server.chat.source_router import resolve_sources
10+
from server.db.postgres import PostgresClient
11+
from server.models.chat_config import RecallConfig
12+
from server.models.retrieval import ChunkMatch
13+
from server.models.tribrid_config_model import ChatRequest, TriBridConfig
14+
from server.services.conversation_store import Conversation
15+
from server.services.rag import FusionProtocol
16+
17+
18+
def _build_system_prompt(*, config: TriBridConfig, corpus_ids: list[str]) -> str:
19+
chat_cfg = config.chat
20+
prompt = str(chat_cfg.system_prompt_base or "")
21+
22+
recall_id = str(chat_cfg.recall.default_corpus_id or "recall_default")
23+
has_recall = recall_id in set(corpus_ids)
24+
has_rag = any(cid and cid != recall_id for cid in corpus_ids)
25+
26+
if has_recall:
27+
prompt += str(chat_cfg.system_prompt_recall_suffix or "")
28+
if has_rag:
29+
prompt += str(chat_cfg.system_prompt_rag_suffix or "")
30+
return prompt.strip() or "You are a helpful assistant."
31+
32+
33+
async def _ensure_recall_ready(pg: PostgresClient, recall_cfg: RecallConfig) -> None:
34+
# Ensure Recall corpus exists before any retrieval/indexing attempts.
35+
await pg.connect()
36+
from server.chat.recall_indexer import ensure_recall_corpus
37+
38+
await ensure_recall_corpus(pg, recall_cfg)
39+
40+
41+
def _should_index_recall(*, recall_cfg: RecallConfig, corpus_ids: list[str]) -> bool:
42+
if not recall_cfg.enabled:
43+
return False
44+
recall_id = str(recall_cfg.default_corpus_id or "recall_default")
45+
return recall_id in set(corpus_ids)
46+
47+
48+
async def chat_once(
49+
*,
50+
request: ChatRequest,
51+
config: TriBridConfig,
52+
fusion: FusionProtocol,
53+
conversation: Conversation,
54+
) -> tuple[str, list[ChunkMatch], str | None]:
55+
"""Non-streaming chat handler."""
56+
57+
corpus_ids = resolve_sources(request.sources)
58+
59+
# Ensure recall corpus exists before retrieval/indexing if enabled + selected.
60+
pg = PostgresClient(config.indexing.postgres_url)
61+
if _should_index_recall(recall_cfg=config.chat.recall, corpus_ids=corpus_ids):
62+
await _ensure_recall_ready(pg, config.chat.recall)
63+
64+
# Retrieval (skip when nothing checked)
65+
sources: list[ChunkMatch] = []
66+
if corpus_ids:
67+
sources = await fusion.search(
68+
corpus_ids,
69+
request.message,
70+
config.fusion,
71+
include_vector=bool(request.include_vector),
72+
include_sparse=bool(request.include_sparse),
73+
include_graph=bool(request.include_graph),
74+
top_k=request.top_k,
75+
)
76+
77+
# Provider + prompt
78+
system_prompt = _build_system_prompt(config=config, corpus_ids=corpus_ids)
79+
route = select_provider_route(chat_config=config.chat, model_override=request.model_override)
80+
temperature = (
81+
float(config.chat.temperature_no_retrieval) if not corpus_ids else float(config.chat.temperature)
82+
)
83+
84+
text, provider_id = await generate_chat_text(
85+
route=route,
86+
openrouter_cfg=config.chat.openrouter,
87+
system_prompt=system_prompt,
88+
user_message=request.message,
89+
images=list(request.images or []),
90+
temperature=temperature,
91+
max_tokens=int(config.chat.max_tokens),
92+
context_chunks=sources,
93+
timeout_s=float(getattr(config.ui, "chat_stream_timeout", 120) or 120),
94+
)
95+
96+
# Update in-memory conversation continuity (best-effort for local providers)
97+
if provider_id:
98+
conversation.last_provider_response_id = provider_id
99+
100+
return text, sources, provider_id
101+
102+
103+
async def chat_stream(
104+
*,
105+
request: ChatRequest,
106+
config: TriBridConfig,
107+
fusion: FusionProtocol,
108+
conversation: Conversation,
109+
run_id: str,
110+
started_at_ms: int,
111+
) -> AsyncIterator[str]:
112+
"""Streaming chat handler that yields SSE events (type=text/done/error)."""
113+
114+
corpus_ids = resolve_sources(request.sources)
115+
116+
pg = PostgresClient(config.indexing.postgres_url)
117+
if _should_index_recall(recall_cfg=config.chat.recall, corpus_ids=corpus_ids):
118+
await _ensure_recall_ready(pg, config.chat.recall)
119+
120+
# Retrieval (skip when nothing checked)
121+
sources: list[ChunkMatch] = []
122+
if corpus_ids:
123+
sources = await fusion.search(
124+
corpus_ids,
125+
request.message,
126+
config.fusion,
127+
include_vector=bool(request.include_vector),
128+
include_sparse=bool(request.include_sparse),
129+
include_graph=bool(request.include_graph),
130+
top_k=request.top_k,
131+
)
132+
133+
system_prompt = _build_system_prompt(config=config, corpus_ids=corpus_ids)
134+
route = select_provider_route(chat_config=config.chat, model_override=request.model_override)
135+
temperature = (
136+
float(config.chat.temperature_no_retrieval) if not corpus_ids else float(config.chat.temperature)
137+
)
138+
139+
accumulated = ""
140+
try:
141+
async for delta in stream_chat_text(
142+
route=route,
143+
openrouter_cfg=config.chat.openrouter,
144+
system_prompt=system_prompt,
145+
user_message=request.message,
146+
images=list(request.images or []),
147+
temperature=temperature,
148+
max_tokens=int(config.chat.max_tokens),
149+
context_chunks=sources,
150+
timeout_s=float(getattr(config.ui, "chat_stream_timeout", 120) or 120),
151+
):
152+
accumulated += delta
153+
yield f"data: {json.dumps({'type': 'text', 'content': delta})}\n\n"
154+
155+
ended_at_ms = int(time.time() * 1000)
156+
sources_json = [s.model_dump(mode="serialization", by_alias=True) for s in sources]
157+
done_payload = {
158+
"type": "done",
159+
"run_id": run_id,
160+
"started_at_ms": int(started_at_ms),
161+
"ended_at_ms": int(ended_at_ms),
162+
"conversation_id": conversation.id,
163+
"sources": sources_json,
164+
}
165+
yield f"data: {json.dumps(done_payload)}\n\n"
166+
167+
except Exception as e:
168+
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
169+

0 commit comments

Comments
 (0)