Skip to content

Commit a5769da

Browse files
authored
Foxhollow Dispatch tool choice, entity tagging, readme updates (#31)
* widget action followup * event finder tool choice * added puzzle and title agents * article tagging * news guide cleanup * updated readmes * progress update formatting * small fixes * event store cleanup * ruff format * types fix
1 parent c7aa6ef commit a5769da

37 files changed

+1662
-258
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ You can run the following examples:
66

77
- [**Cat Lounge**](examples/cat-lounge) - caretaker for a virtual cat that helps improve energy, happiness, and cleanliness stats.
88
- [**Customer Support**](examples/customer-support) – airline concierge with live itinerary data, timeline syncing, and domain-specific tools.
9+
- [**News Guide**](examples/news-guide) – Foxhollow Dispatch newsroom assistant with article search, @-mentions, and page-aware responses.
910

1011
## Quickstart
1112

@@ -17,19 +18,37 @@ You can run the following examples:
1718
| ---------------- | -------------------------- | ---------------------------------------------------------- | --------------------- |
1819
| Cat Lounge | `npm run cat-lounge` | `cd examples/cat-lounge && npm install && npm run start` | http://localhost:5170 |
1920
| Customer Support | `npm run customer-support` | `cd examples/customer-support && npm install && npm start` | http://localhost:5171 |
21+
| News Guide | `npm run news-guide` | `cd examples/news-guide && npm install && npm run start` | http://localhost:5172 |
2022

2123
## Feature index
2224

2325
### Server tool calls to retrieve application data for inference
2426

2527
- **Cat Lounge**:
2628
- Function tool `get_cat_status` ([cat_agent.py](examples/cat-lounge/backend/app/cat_agent.py)) pulls the latest cat stats for the agent.
29+
- **News Guide**:
30+
- The agent leans on a suite of retrieval tools—`list_available_tags_and_keywords`, `get_article_by_id`, `search_articles_by_tags/keywords/exact_text`, and `get_current_page`—before responding, and uses `show_article_list_widget` to present results ([news_agent.py](examples/news-guide/backend/app/agents/news_agent.py)).
31+
- Hidden context such as the featured landing page is normalized into agent input so summaries and recommendations stay grounded ([news_agent.py](examples/news-guide/backend/app/agents/news_agent.py)).
2732

2833
### Client tool calls that mutate UI state
2934

3035
- **Cat Lounge**:
3136
- Client tool `update_cat_status` is invoked by server tools `feed_cat`, `play_with_cat`, `clean_cat`, and `speak_as_cat` to sync UI state.
3237
- When invoked, it is handled client-side with the `handleClientToolCall` callback in [ChatKitPanel.tsx](examples/cat-lounge/frontend/src/components/ChatKitPanel.tsx).
38+
- **News Guide**:
39+
- The `open_article` widget action triggers client-side navigation to the selected story and forwards the action back to the server via `sendCustomAction` to follow up with context-aware prompts ([ChatKitPanel.tsx](examples/news-guide/frontend/src/components/ChatKitPanel.tsx)).
40+
41+
### Page-aware model responses
42+
43+
- **News Guide**:
44+
- The ChatKit client forwards the currently open article id in an `article-id` header so the backend can scope responses to “this page” ([ChatKitPanel.tsx](examples/news-guide/frontend/src/components/ChatKitPanel.tsx)).
45+
- The server reads that request context and exposes `get_current_page` so the agent can load full content without asking the user to paste it ([main.py](examples/news-guide/backend/app/main.py), [news_agent.py](examples/news-guide/backend/app/agents/news_agent.py)).
46+
47+
### Progress updates
48+
49+
- **News Guide**:
50+
- Retrieval tools stream `ProgressUpdateEvent` messages while searching tags, authors, keywords, exact text, or loading the current page so the UI surfaces “Searching…”/“Loading…” states ([news_agent.py](examples/news-guide/backend/app/agents/news_agent.py)).
51+
- The event finder emits progress as it scans dates, days of week, or keywords to keep users informed during longer lookups ([event_finder_agent.py](examples/news-guide/backend/app/agents/event_finder_agent.py)).
3352

3453
### Widgets without actions
3554

@@ -41,9 +60,28 @@ You can run the following examples:
4160
- **Cat Lounge**:
4261
- Server tool `suggest_cat_names` streams a widget with action configs that specify `cats.select_name` and `cats.more_names` client-handled actions.
4362
- When the user clicks the widget, these actions are handled with the `handleWidgetAction` callback in [ChatKitPanel.tsx](examples/cat-lounge/frontend/src/components/ChatKitPanel.tsx).
63+
- **News Guide**:
64+
- Article list widgets render “View” buttons that dispatch `open_article` actions for client navigation and engagement ([news_agent.py](examples/news-guide/backend/app/agents/news_agent.py), [article_list_widget.py](examples/news-guide/backend/app/widgets/article_list_widget.py)).
65+
- The event finder streams a timeline widget with `view_event_details` buttons configured for server handling so users can expand items inline ([event_finder_agent.py](examples/news-guide/backend/app/agents/event_finder_agent.py), [event_list_widget.py](examples/news-guide/backend/app/widgets/event_list_widget.py)).
4466

4567
### Server-handled widget actions
4668

4769
- **Cat Lounge**:
4870
- The `cats.select_name` action is also handled server-side to reflect updates to data and stream back an updated version of the name suggestions widget in [server.py](examples/cat-lounge/backend/app/server.py).
4971
- It is invoked using `chatkit.sendAction()` from `handleWidgetAction` callback in [ChatKitPanel.tsx](examples/cat-lounge/frontend/src/components/ChatKitPanel.tsx).
72+
- **News Guide**:
73+
- The `view_event_details` action is processed server-side to update the timeline widget with expanded descriptions without a round trip to the model ([server.py](examples/news-guide/backend/app/server.py)).
74+
75+
### Entity tags (@-mentions)
76+
77+
- **News Guide**:
78+
- Entity search and previews power @-mentions for articles/authors in the composer and render hover previews via `/articles/tags` ([ChatKitPanel.tsx](examples/news-guide/frontend/src/components/ChatKitPanel.tsx), [main.py](examples/news-guide/backend/app/main.py)).
79+
- Tagged entities are converted into model-readable markers so the agent can fetch the right records (`<ARTICLE_REFERENCE>` / `<AUTHOR_REFERENCE>`) ([thread_item_converter.py](examples/news-guide/backend/app/thread_item_converter.py)).
80+
- Article reference tags are resolved into full articles via the instructed `get_article_by_id` tool before the agent cites details ([news_agent.py](examples/news-guide/backend/app/agents/news_agent.py)).
81+
82+
### Tool choice (composer menu)
83+
84+
- **News Guide**:
85+
- The ChatKit client is configured with a `composer.tools` option that specifies options in the composer menu ([ChatKitPanel.tsx](examples/news-guide/frontend/src/components/ChatKitPanel.tsx))
86+
- Composer tool buttons let users force specific agents (`event_finder`, `puzzle`), setting `tool_choice` on the request ([config.ts](examples/news-guide/frontend/src/lib/config.ts)).
87+
- The backend routes these tool choices to specialized agents before falling back to the News Guide agent ([server.py](examples/news-guide/backend/app/server.py)).

examples/cat-lounge/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Cat Lounge
2+
3+
Virtual cat caretaker demo built with ChatKit (FastAPI backend + Vite/React frontend).
4+
5+
## Quickstart
6+
7+
1. Export `OPENAI_API_KEY`.
8+
2. From the repo root run `npm run cat-lounge` (or `cd examples/cat-lounge && npm install && npm run start`).
9+
3. Go to http://localhost:5170
10+
11+
## Example prompts
12+
13+
- "Feed the cat a tuna treat."
14+
- "The cat looks a little messy—give them a bath."
15+
- "What should I name the cat?"
16+
- "Can I see the cat's profile card?"
17+
- "Hello, cat! How are you feeling?"
18+
19+
## Features
20+
21+
- Server tools to read and mutate per-thread cat state: `get_cat_status`, `feed_cat`, `play_with_cat`, `clean_cat`, `set_cat_name`, `speak_as_cat`.
22+
- Name suggestion workflow with a selectable widget and client-handled actions (`cats.select_name`, `cats.more_names`) plus server reconciliation for chosen names.
23+
- Profile card widget (`show_cat_profile`) streamed from the server with presentation-only content.
24+
- Client tool call `update_cat_status` keeps the UI stats in sync after each server tool invocation.
25+
- Hidden context tags track recent actions (<FED_CAT>, <PLAYED_WITH_CAT>, <CLEANED_CAT>, <CAT_NAME_SELECTED>) so the agent remembers what already happened.
26+
- Quick actions call `chatkit.sendUserMessage` to send canned requests without typing ([App.tsx](frontend/src/App.tsx)).

examples/news-guide/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# News Guide
2+
3+
Foxhollow Dispatch newsroom assistant showcasing retrieval-heavy ChatKit flows and rich widgets.
4+
5+
## Quickstart
6+
7+
1. Export `OPENAI_API_KEY` (and `VITE_CHATKIT_API_DOMAIN_KEY=domain_pk_local_dev` for local).
8+
2. From the repo root run `npm run news-guide` (or `cd examples/news-guide && npm install && npm run start`).
9+
3. Go to: http://localhost:5172
10+
11+
## Example prompts
12+
13+
- "What's trending right now?" (article search + list widget)
14+
- "Summarize this page for me." (page-aware `get_current_page`)
15+
- "Show me everything tagged parks and outdoor events." (information retrieval using tools)
16+
- "@Elowen latest stories?" (author @-mention lookup; only works when manually typing @, no copy paste)
17+
- "What events are happening this Saturday?" (select the "Event finder" tool from the composer menu first)
18+
- "Give me a quick puzzle break." (select the "Coffee break puzzle" tool from the composer menu)
19+
20+
## Features
21+
22+
- Retrieval tool suite for metadata and content (`list_available_tags_and_keywords`, `search_articles_by_tags/keywords/exact_text`, `search_articles_by_author`), plus article list widgets to present results.
23+
- Page-aware context via `article-id` request header and `get_current_page` tool for grounded answers about the open article, using the custom fetch ChatKit option.
24+
- Entity tags with previews; tagged articles/authors become `<ARTICLE_REFERENCE>` / `<AUTHOR_REFERENCE>` markers that drive `get_article_by_id`.
25+
- Progress streaming (`ProgressUpdateEvent`) during searches and page loads to keep the UI responsive.
26+
- Composer tool options for explicit agent routing (`event_finder`, `puzzle`) using `tool_choice`.
27+
- Widgets with client and server actions: article list "View" buttons (`open_article`) and event timeline with server-handled `view_event_details` updates.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from typing import Annotated, Any, List
5+
6+
from agents import Agent, RunContextWrapper, StopAtTools, function_tool
7+
from chatkit.agents import AgentContext
8+
from chatkit.types import (
9+
AssistantMessageContent,
10+
AssistantMessageItem,
11+
ProgressUpdateEvent,
12+
ThreadItemDoneEvent,
13+
)
14+
from pydantic import BaseModel, ConfigDict, Field
15+
16+
from ..data.event_store import EventRecord, EventStore
17+
from ..memory_store import MemoryStore
18+
from ..request_context import RequestContext
19+
from ..widgets.event_list_widget import build_event_list_widget
20+
21+
INSTRUCTIONS = """
22+
You help Foxhollow residents discover local happenings. When a reader asks for events,
23+
search the curated calendar, call out dates and notable details, and keep recommendations brief.
24+
25+
Use the available tools deliberately:
26+
- Call `list_available_event_keywords` to get the full set of event keywords and categories,
27+
fuzzy match the reader's phrasing to the closest options (case-insensitive, partial matches are ok),
28+
then feed those terms into a keyword search instead of relying on hard-coded synonyms.
29+
- If they mention a specific date (YYYY-MM-DD), start with `search_events_by_date`.
30+
- If they reference a day of the week, try `search_events_by_day_of_week`.
31+
- For general vibes (e.g., “family friendly night markets”), use `search_events_by_keyword`
32+
so the search spans titles, categories, locations, and curated keywords.
33+
34+
Whenever a search tool returns more than one event immediately call `show_event_list_widget`
35+
with those results before sending your final text, along with a 1-sentence message explaining why these events were selected.
36+
This ensures every response ships with the timeline widget.
37+
Cite event titles in bold, mention the date, and highlight one delightful detail when replying.
38+
39+
When the user explicitly asks for more details on the events, you MUST describe the events in natural language
40+
without using the `show_event_list_widget` tool.
41+
"""
42+
43+
MODEL = "gpt-4.1-mini"
44+
45+
46+
class EventFinderContext(AgentContext):
47+
model_config = ConfigDict(arbitrary_types_allowed=True)
48+
store: Annotated[MemoryStore, Field(exclude=True)]
49+
events: Annotated[EventStore, Field(exclude=True)]
50+
request_context: Annotated[RequestContext, Field(exclude=True, default_factory=RequestContext)]
51+
52+
53+
class EventKeywords(BaseModel):
54+
keywords: List[str]
55+
56+
57+
@function_tool(
58+
description_override="Find scheduled events happening on a specific date (YYYY-MM-DD)."
59+
)
60+
async def search_events_by_date(
61+
ctx: RunContextWrapper[EventFinderContext],
62+
date: str,
63+
) -> dict[str, Any]:
64+
print("[TOOL CALL] search_events_by_date", date)
65+
if not date:
66+
raise ValueError("Provide a valid date in YYYY-MM-DD format.")
67+
await ctx.context.stream(ProgressUpdateEvent(text=f"Looking up events on {date}"))
68+
records = ctx.context.events.search_by_date(date)
69+
return {"events": _events_to_json(records)}
70+
71+
72+
@function_tool(description_override="List events occurring on a given day of the week.")
73+
async def search_events_by_day_of_week(
74+
ctx: RunContextWrapper[EventFinderContext],
75+
day: str,
76+
) -> dict[str, Any]:
77+
print("[TOOL CALL] search_events_by_day_of_week", day)
78+
if not day:
79+
raise ValueError("Provide a day of the week to search for (e.g., Saturday).")
80+
await ctx.context.stream(ProgressUpdateEvent(text=f"Checking {day} events"))
81+
records = ctx.context.events.search_by_day_of_week(day)
82+
return {"events": _events_to_json(records)}
83+
84+
85+
@function_tool(
86+
description_override="Search events with general keywords (title, category, location, or details)."
87+
)
88+
async def search_events_by_keyword(
89+
ctx: RunContextWrapper[EventFinderContext],
90+
keywords: List[str],
91+
) -> dict[str, Any]:
92+
print("[TOOL CALL] search_events_by_keyword", keywords)
93+
tokens = [keyword.strip() for keyword in keywords if keyword and keyword.strip()]
94+
if not tokens:
95+
raise ValueError("Provide at least one keyword to search for.")
96+
label = ", ".join(tokens)
97+
await ctx.context.stream(ProgressUpdateEvent(text=f"Searching for: {label}"))
98+
records = ctx.context.events.search_by_keyword(tokens)
99+
return {"events": _events_to_json(records)}
100+
101+
102+
@function_tool(description_override="List all unique event keywords and categories.")
103+
async def list_available_event_keywords(
104+
ctx: RunContextWrapper[EventFinderContext],
105+
) -> EventKeywords:
106+
print("[TOOL CALL] list_available_event_keywords")
107+
await ctx.context.stream(ProgressUpdateEvent(text="Referencing available event keywords..."))
108+
return EventKeywords(keywords=ctx.context.events.list_available_keywords())
109+
110+
111+
@function_tool(description_override="Show a timeline-styled widget for a provided set of events.")
112+
async def show_event_list_widget(
113+
ctx: RunContextWrapper[EventFinderContext],
114+
events: List[EventRecord],
115+
message: str | None = None,
116+
):
117+
print("[TOOL CALL] show_event_list_widget", events)
118+
records: List[EventRecord] = [event for event in events if event]
119+
120+
# Gracefully handle case where agent mistakenly calls this tool with no events.
121+
# Otherewise, since the agent is configured to stop running after this tool call, the user
122+
# will never see further output.
123+
if not records:
124+
fallback = message or "I couldn't find any events that match that search."
125+
await ctx.context.stream(
126+
ThreadItemDoneEvent(
127+
item=AssistantMessageItem(
128+
thread_id=ctx.context.thread.id,
129+
id=ctx.context.generate_id("message"),
130+
created_at=datetime.now(),
131+
content=[AssistantMessageContent(text=fallback)],
132+
),
133+
)
134+
)
135+
136+
widget = build_event_list_widget(records)
137+
copy_text = ", ".join(filter(None, (event.title for event in records)))
138+
await ctx.context.stream_widget(widget, copy_text=copy_text or "Local events")
139+
140+
summary = message or "Here are the events that match your request."
141+
await ctx.context.stream(
142+
ThreadItemDoneEvent(
143+
item=AssistantMessageItem(
144+
thread_id=ctx.context.thread.id,
145+
id=ctx.context.generate_id("message"),
146+
created_at=datetime.now(),
147+
content=[AssistantMessageContent(text=summary)],
148+
),
149+
)
150+
)
151+
152+
153+
def _events_to_json(events: List[EventRecord]) -> List[dict[str, Any]]:
154+
"""Convert EventRecord models to JSON-safe dicts for tool responses."""
155+
return [event.model_dump(mode="json", by_alias=True) for event in events]
156+
157+
158+
class EventSummaryContext(AgentContext):
159+
model_config = ConfigDict(arbitrary_types_allowed=True)
160+
store: Annotated[MemoryStore, Field(exclude=True)]
161+
events: Annotated[EventStore, Field(exclude=True)]
162+
request_context: Annotated[RequestContext, Field(exclude=True, default_factory=RequestContext)]
163+
164+
165+
event_finder_agent = Agent[EventFinderContext](
166+
model=MODEL,
167+
name="Foxhollow Event Finder",
168+
instructions=INSTRUCTIONS,
169+
tools=[
170+
search_events_by_date,
171+
search_events_by_day_of_week,
172+
search_events_by_keyword,
173+
list_available_event_keywords,
174+
show_event_list_widget,
175+
],
176+
# Stop inference after showing the event list widget to prevent content from being repeated in a continued response.
177+
tool_use_behavior=StopAtTools(stop_at_tool_names=[show_event_list_widget.name]),
178+
)

0 commit comments

Comments
 (0)