Skip to content

Commit d471226

Browse files
abossardCopilot
andauthored
feat: add ThreadListSidebar component for managing conversations (#29)
* feat: add ThreadListSidebar component for managing conversations feat: implement tool render components for AG-UI with various widgets feat: create useRunManager hook for managing agent run lifecycle feat: add CopilotKitProvider for AG-UI context integration test: implement e2e tests for conversation functionality and thread API Signed-off-by: Andre Bossard <anbossar@microsoft.com> * feat: smart JSON rendering in chat + auto-continue button - Add SmartMessageRenderer that detects JSON blocks in markdown and renders them as structured tables/cards instead of raw <pre><code> - Apply SmartMessageRenderer in ConversationPanel and RunConversationModal - Add 'Continue in Chat' button on AgentRunPage after a run completes - Add Playwright tests for both features (mocked, fast, deterministic) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove nested scrolling in chat/result views Remove overflow:auto and maxHeight from all inner containers (assistantMessage, widgetContainer, json, jsonPre) so content flows flat within the outer scroll context. Browser handles scrolling naturally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Implement GitHub Copilot authentication and integrate with LangChain - Added CopilotAuthenticator for OAuth device-flow authentication with GitHub Copilot API. - Created functions to manage access tokens and API keys, including caching mechanisms. - Developed Copilot LLM factory to build LangChain clients for GitHub Copilot. - Introduced SSE connection management with a singleton pattern for real-time event handling. - Refactored activity and run management to utilize new SSE connection for improved event handling. - Updated frontend components to handle connection states and errors more effectively. - Cleaned up setup script by removing unnecessary instructions related to LLM backend configuration. Signed-off-by: Andre Bossard <anbossar@microsoft.com> * Improve GitHub device flow terminal output with clear banner and progress - Suppress httpx INFO logs during device flow to avoid drowning the message - Display a box-drawn banner with numbered steps (URL + user code) - Show polling progress indicator so terminal doesn't appear frozen - Print success confirmation on authentication - Respect server's poll interval and increase timeout to 2 minutes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: Update README with new setup instructions and dependency changes Signed-off-by: Andre Bossard <anbossar@microsoft.com> * fix: Update StatCardWidget to handle null and object values for display Signed-off-by: Andre Bossard <anbossar@microsoft.com> --------- Signed-off-by: Andre Bossard <anbossar@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 593f45f commit d471226

99 files changed

Lines changed: 4806 additions & 9023 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
"Bash(npx playwright test --project=chromium --reporter=line)",
1717
"Bash(npx playwright test:*)",
1818
"Bash(find:*)",
19-
"Bash(python:*)"
19+
"Bash(python:*)",
20+
"WebFetch(domain:github.com)",
21+
"WebFetch(domain:pypi.org)",
22+
"WebFetch(domain:deepwiki.com)",
23+
"Bash(pip list:*)"
2024
],
2125
"deny": [],
2226
"ask": []

.env.example

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
# LiteLLM Configuration (Default LLM backend)
2-
# Works out of the box with GitHub Copilot — no API keys needed.
3-
# Supports 100+ providers: GitHub Copilot, Ollama, Anthropic, etc.
4-
# Docs: https://docs.litellm.ai/docs/providers
1+
# GitHub Copilot Configuration (Default LLM backend)
2+
# Authentication: on first run, you'll be prompted to authenticate via device flow.
3+
# Tokens are cached at ~/.config/copilot-llm/ and auto-refresh.
4+
# Note: Regular GitHub PATs do NOT work with the Copilot Chat API.
55

6-
# Primary model (default: github_copilot/gpt-4o)
7-
# LITELLM_MODEL=github_copilot/gpt-4o
6+
# Primary model (default: gpt-4o)
7+
# COPILOT_MODEL=gpt-4o
88

99
# Fallback chain (comma-separated, tried in order if primary fails)
10-
# LITELLM_FALLBACK_MODELS=github_copilot/claude-sonnet-4,github_copilot/gpt-4o,github_copilot/gpt-4o-mini
10+
# COPILOT_FALLBACK_MODELS=claude-sonnet-4,gpt-4o,gpt-4o-mini
1111

12-
# OpenAI Configuration (Optional — overrides LiteLLM when set)
12+
# OpenAI Configuration (Optional — overrides Copilot when AGENT_BACKEND=openai)
1313
# Get your API key from: https://platform.openai.com/api-keys
1414
# If set, uses OpenAI SDK directly with beta.chat.completions.parse()
1515

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ The `@operation` decorator auto-generates: REST endpoint, MCP JSON-RPC method, J
7676
- `agent_builder/` — Full agent lifecycle: `service.py` (WorkbenchService), `engine/` (ReAct loop), `persistence/` (SQLite repos), `tools/` (ToolRegistry, MCP adapters), `routes.py` (Blueprint at `/api/workbench/*`)
7777
- `kba_service.py` — Knowledge Base Article generation pipeline
7878

79-
**LLM config**: LiteLLM is default (works with GitHub Copilot, no key needed); OpenAI is optional override. Config via `.env`.
79+
**LLM config**: GitHub Copilot is default (uses device flow or GITHUB_TOKEN); OpenAI is optional override. Config via `.env`.
8080

8181
**MCP**: Backend exposes both REST and MCP JSON-RPC from the same `@operation` handlers. `mcp_handler.py` routes JSON-RPC to operations.
8282

@@ -105,8 +105,8 @@ Backend tests use pytest. `agent_builder/tests/` has 132 tests covering the agen
105105

106106
Copy `.env.example` to `.env`. Key variables:
107107
```
108-
# LiteLLM (default — no key required if using GitHub Copilot)
109-
LITELLM_MODEL=github_copilot/gpt-4o
108+
# GitHub Copilot (default — uses GITHUB_TOKEN or device flow)
109+
COPILOT_MODEL=gpt-4o
110110
111111
# OpenAI (optional override)
112112
OPENAI_API_KEY=sk-proj-...

README.md

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
# Quart + Vite + React Demo Application
1+
# UPDATE
2+
> [!WARNING]
3+
> This version has new dependencies and not anymore to LiteLLM.
4+
> Please delete the current .venv folders and run the ./setup.sh
5+
> Check the logs of the Python process, you might need to authenticate to Github.com so that
6+
> the application can use the Github token
7+
8+
rm -rf .venv
9+
rm -rf notebooks/
10+
./setup.sh
11+
./start-dev.sh
212

3-
**Current Task:** Document usecase demo ideas from the CSV-backed ticket dataset and build/iterate pages like `/usecase_demo_1` where each demo page has:
4-
- a short summary
5-
- editable agent prompt(s)
6-
- a button that launches the agent run in background
7-
- visible results (table/visualization)
13+
----------
14+
15+
# Quart + Vite + React Demo Application
816

917
> A teaching-oriented full-stack sample that pairs a Python Quart backend with a React + FluentUI frontend, real-time Server-Sent Events (SSE), and Playwright tests.
1018
@@ -43,29 +51,6 @@ All deep-dive guides now live under `docs/` for easier discovery:
4351
- **[Publishing Guide](docs/KBA_PUBLISHING.md)** – How to publish KBAs to different KB systems
4452

4553

46-
### DSPy Prompt Tuning Playground
47-
48-
> **NEW:** Interactive Jupyter notebook series teaching prompt optimization with DSPy
49-
50-
- **[Playground README](notebooks/README.md)** – Setup, learning path, and task catalog
51-
52-
#### Quick start
53-
54-
```bash
55-
source .venv/bin/activate
56-
pip install -r notebooks/requirements.txt
57-
cd notebooks && jupyter lab
58-
```
59-
60-
Open `00_introduction.ipynb` and follow the 8-notebook learning path. The notebooks automatically discover available models from your `.env` LiteLLM config — no hardcoded API keys needed.
61-
62-
#### What's inside
63-
64-
- **8 notebooks** organized by learning concept (Grokking Simplicity, Philosophy of Software Design)
65-
- **20 LLM tasks** across 4 tiers (Basics → Reasoning → Composition → Agentic)
66-
- **ipywidgets** for interactive model selection, optimization controls, and prompt diff viewing
67-
- **Plotly charts** for model comparison, optimization progress, and ROI calculation
68-
- Uses the same `LITELLM_MODEL` / `LITELLM_FALLBACK_MODELS` env vars as the backend
6954

7055

7156
## 5-minute quick start (TL;DR)

backend/agent_builder/chat_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _default_model(explicit_model: str = "") -> str:
3737
return explicit_model
3838
if os.getenv("AGENT_BACKEND", "").strip().lower() == "openai":
3939
return os.getenv("OPENAI_MODEL", "gpt-4o-mini")
40-
return os.getenv("LITELLM_MODEL", "github_copilot/gpt-4o")
40+
return os.getenv("COPILOT_MODEL", "gpt-4o")
4141

4242

4343
class ChatService:
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""
2+
Agent Builder — AG-UI Event Conversion
3+
4+
Calculations: pure functions that convert ReAct execution data
5+
into AG-UI protocol events. No I/O, no database, no side effects.
6+
Each function takes data in, returns AG-UI event(s) out.
7+
"""
8+
9+
import json
10+
import uuid
11+
from typing import Any, Optional
12+
13+
from ag_ui.core import (
14+
CustomEvent,
15+
EventType,
16+
RunErrorEvent,
17+
RunFinishedEvent,
18+
RunStartedEvent,
19+
StepFinishedEvent,
20+
StepStartedEvent,
21+
TextMessageContentEvent,
22+
TextMessageEndEvent,
23+
TextMessageStartEvent,
24+
ToolCallArgsEvent,
25+
ToolCallEndEvent,
26+
ToolCallResultEvent,
27+
ToolCallStartEvent,
28+
StateSnapshotEvent,
29+
)
30+
from ag_ui.encoder import EventEncoder
31+
32+
33+
# Shared encoder — stateless, safe to reuse
34+
_encoder = EventEncoder()
35+
36+
37+
# ---------------------------------------------------------------------------
38+
# Pure conversion functions
39+
# ---------------------------------------------------------------------------
40+
41+
def run_started_event(thread_id: str, run_id: str) -> RunStartedEvent:
42+
return RunStartedEvent(
43+
type=EventType.RUN_STARTED,
44+
thread_id=thread_id,
45+
run_id=run_id,
46+
)
47+
48+
49+
def run_finished_event(thread_id: str, run_id: str) -> RunFinishedEvent:
50+
return RunFinishedEvent(
51+
type=EventType.RUN_FINISHED,
52+
thread_id=thread_id,
53+
run_id=run_id,
54+
)
55+
56+
57+
def run_error_event(message: str) -> RunErrorEvent:
58+
return RunErrorEvent(
59+
type=EventType.RUN_ERROR,
60+
message=message,
61+
)
62+
63+
64+
def text_message_start(message_id: str, role: str = "assistant") -> TextMessageStartEvent:
65+
return TextMessageStartEvent(
66+
type=EventType.TEXT_MESSAGE_START,
67+
message_id=message_id,
68+
role=role,
69+
)
70+
71+
72+
def text_message_content(message_id: str, delta: str) -> TextMessageContentEvent:
73+
return TextMessageContentEvent(
74+
type=EventType.TEXT_MESSAGE_CONTENT,
75+
message_id=message_id,
76+
delta=delta,
77+
)
78+
79+
80+
def text_message_end(message_id: str) -> TextMessageEndEvent:
81+
return TextMessageEndEvent(
82+
type=EventType.TEXT_MESSAGE_END,
83+
message_id=message_id,
84+
)
85+
86+
87+
def text_message_events(content: str, message_id: Optional[str] = None) -> list:
88+
"""Convert a complete text into the three-event TEXT_MESSAGE sequence."""
89+
msg_id = message_id or str(uuid.uuid4())
90+
return [
91+
text_message_start(msg_id, role="assistant"),
92+
text_message_content(msg_id, content),
93+
text_message_end(msg_id),
94+
]
95+
96+
97+
def tool_call_start(tool_call_id: str, tool_name: str, parent_message_id: Optional[str] = None) -> ToolCallStartEvent:
98+
return ToolCallStartEvent(
99+
type=EventType.TOOL_CALL_START,
100+
tool_call_id=tool_call_id,
101+
tool_call_name=tool_name,
102+
parent_message_id=parent_message_id,
103+
)
104+
105+
106+
def tool_call_args(tool_call_id: str, args: Any) -> ToolCallArgsEvent:
107+
args_str = json.dumps(args) if not isinstance(args, str) else args
108+
return ToolCallArgsEvent(
109+
type=EventType.TOOL_CALL_ARGS,
110+
tool_call_id=tool_call_id,
111+
delta=args_str,
112+
)
113+
114+
115+
def tool_call_end(tool_call_id: str) -> ToolCallEndEvent:
116+
return ToolCallEndEvent(
117+
type=EventType.TOOL_CALL_END,
118+
tool_call_id=tool_call_id,
119+
)
120+
121+
122+
def tool_call_result(tool_call_id: str, content: str, message_id: Optional[str] = None) -> ToolCallResultEvent:
123+
msg_id = message_id or str(uuid.uuid4())
124+
return ToolCallResultEvent(
125+
type=EventType.TOOL_CALL_RESULT,
126+
message_id=msg_id,
127+
tool_call_id=tool_call_id,
128+
content=content,
129+
role="tool",
130+
)
131+
132+
133+
def tool_call_events(
134+
tool_name: str,
135+
args: Any,
136+
result: str,
137+
tool_call_id: Optional[str] = None,
138+
parent_message_id: Optional[str] = None,
139+
) -> list:
140+
"""Convert a complete tool invocation into the four-event TOOL_CALL sequence."""
141+
tc_id = tool_call_id or str(uuid.uuid4())
142+
return [
143+
tool_call_start(tc_id, tool_name, parent_message_id),
144+
tool_call_args(tc_id, args),
145+
tool_call_end(tc_id),
146+
tool_call_result(tc_id, result),
147+
]
148+
149+
150+
def step_started_event(step_name: str) -> StepStartedEvent:
151+
return StepStartedEvent(
152+
type=EventType.STEP_STARTED,
153+
step_name=step_name,
154+
)
155+
156+
157+
def step_finished_event(step_name: str) -> StepFinishedEvent:
158+
return StepFinishedEvent(
159+
type=EventType.STEP_FINISHED,
160+
step_name=step_name,
161+
)
162+
163+
164+
def state_snapshot_event(snapshot: dict[str, Any]) -> StateSnapshotEvent:
165+
return StateSnapshotEvent(
166+
type=EventType.STATE_SNAPSHOT,
167+
snapshot=snapshot,
168+
)
169+
170+
171+
def structured_output_event(
172+
output: Any,
173+
schema: Optional[dict[str, Any]] = None,
174+
) -> CustomEvent:
175+
"""Emit structured agent output as a CUSTOM event with widget metadata.
176+
177+
The frontend uses the schema's x-ui annotations to pick the right
178+
CopilotKit tool render for each field.
179+
"""
180+
return CustomEvent(
181+
type=EventType.CUSTOM,
182+
name="structured_output",
183+
value={
184+
"output": output,
185+
"schema": schema or {},
186+
},
187+
)
188+
189+
190+
# ---------------------------------------------------------------------------
191+
# SSE encoding — thin wrapper, still a pure calculation
192+
# ---------------------------------------------------------------------------
193+
194+
def encode_event(event: Any) -> str:
195+
"""Serialize an AG-UI event to an SSE data line."""
196+
return _encoder.encode(event)

0 commit comments

Comments
 (0)