Skip to content

Commit 8305fec

Browse files
committed
checkpoint
1 parent 2b24bff commit 8305fec

5 files changed

Lines changed: 298 additions & 31 deletions

File tree

agent/main.py

Lines changed: 240 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55

66
import json
7+
import urllib.parse
8+
import urllib.request
79
from collections.abc import Mapping
810
from typing import Any, List, TypedDict
911

@@ -54,9 +56,237 @@ async def apply_structured_output_schema(request, handler):
5456
@tool
5557
def get_weather(location: str):
5658
"""
57-
Get the weather for a given location.
59+
Get the current weather for a given location.
60+
61+
Preferred input format: "City, State, Country" (e.g., "Huntsville, Alabama, USA").
62+
US shorthand is allowed: "City, ST" (e.g., "Huntsville, AL"). The tool will
63+
expand state abbreviations and bias geocoding to the US when a state is detected.
5864
"""
59-
return f"The weather for {location} is 70 degrees."
65+
if not location or not location.strip():
66+
return {
67+
"status": "error",
68+
"message": "Please provide a location in the format \"City, State, Country\".",
69+
"suggestedQueries": [],
70+
}
71+
72+
us_state_map = {
73+
"AL": "Alabama",
74+
"AK": "Alaska",
75+
"AZ": "Arizona",
76+
"AR": "Arkansas",
77+
"CA": "California",
78+
"CO": "Colorado",
79+
"CT": "Connecticut",
80+
"DE": "Delaware",
81+
"FL": "Florida",
82+
"GA": "Georgia",
83+
"HI": "Hawaii",
84+
"ID": "Idaho",
85+
"IL": "Illinois",
86+
"IN": "Indiana",
87+
"IA": "Iowa",
88+
"KS": "Kansas",
89+
"KY": "Kentucky",
90+
"LA": "Louisiana",
91+
"ME": "Maine",
92+
"MD": "Maryland",
93+
"MA": "Massachusetts",
94+
"MI": "Michigan",
95+
"MN": "Minnesota",
96+
"MS": "Mississippi",
97+
"MO": "Missouri",
98+
"MT": "Montana",
99+
"NE": "Nebraska",
100+
"NV": "Nevada",
101+
"NH": "New Hampshire",
102+
"NJ": "New Jersey",
103+
"NM": "New Mexico",
104+
"NY": "New York",
105+
"NC": "North Carolina",
106+
"ND": "North Dakota",
107+
"OH": "Ohio",
108+
"OK": "Oklahoma",
109+
"OR": "Oregon",
110+
"PA": "Pennsylvania",
111+
"RI": "Rhode Island",
112+
"SC": "South Carolina",
113+
"SD": "South Dakota",
114+
"TN": "Tennessee",
115+
"TX": "Texas",
116+
"UT": "Utah",
117+
"VT": "Vermont",
118+
"VA": "Virginia",
119+
"WA": "Washington",
120+
"WV": "West Virginia",
121+
"WI": "Wisconsin",
122+
"WY": "Wyoming",
123+
"DC": "District of Columbia",
124+
}
125+
126+
raw_location = location.strip()
127+
suggested_queries: list[str] = []
128+
country_bias: str | None = None
129+
130+
normalized_location = raw_location
131+
city_only: str | None = None
132+
state_full: str | None = None
133+
parts = [part.strip() for part in raw_location.split(",") if part.strip()]
134+
if len(parts) == 2:
135+
state = parts[1].upper()
136+
if state in us_state_map:
137+
city_only = parts[0]
138+
state_full = us_state_map[state]
139+
normalized_location = f"{parts[0]}, {us_state_map[state]}"
140+
suggested_queries.append(f"{parts[0]}, {us_state_map[state]}, USA")
141+
country_bias = "US"
142+
if len(parts) >= 3:
143+
tail = parts[-1].lower()
144+
if tail in {"usa", "us", "united states", "united states of america"}:
145+
country_bias = "US"
146+
normalized_location = ", ".join(parts[:-1])
147+
suggested_queries.append(f"{normalized_location}, USA")
148+
if len(parts) >= 2:
149+
state_full = parts[-2].strip()
150+
city_only = ", ".join(parts[:-2]).strip()
151+
152+
if raw_location not in suggested_queries:
153+
suggested_queries.append(raw_location)
154+
if normalized_location not in suggested_queries:
155+
suggested_queries.append(normalized_location)
156+
157+
def geocode(name: str, country_code: str | None):
158+
query = urllib.parse.urlencode(
159+
{
160+
"name": name,
161+
"count": 5,
162+
"language": "en",
163+
"format": "json",
164+
**({"countryCode": country_code} if country_code else {}),
165+
}
166+
)
167+
geo_url = f"https://geocoding-api.open-meteo.com/v1/search?{query}"
168+
with urllib.request.urlopen(geo_url, timeout=10) as response:
169+
return json.loads(response.read().decode("utf-8"))
170+
171+
match = None
172+
last_error: Exception | None = None
173+
candidates = [
174+
(normalized_location, country_bias),
175+
(raw_location, country_bias),
176+
(normalized_location, None),
177+
(raw_location, None),
178+
]
179+
if city_only:
180+
candidates.insert(0, (city_only, country_bias))
181+
candidates.append((city_only, None))
182+
for candidate, bias in candidates:
183+
try:
184+
cleaned = candidate
185+
if bias and "," in cleaned:
186+
tail = cleaned.split(",")[-1].strip().lower()
187+
if tail in {"usa", "us", "united states", "united states of america"}:
188+
cleaned = ", ".join(part.strip() for part in cleaned.split(",")[:-1])
189+
geo_data = geocode(cleaned, bias)
190+
results = geo_data.get("results") or []
191+
if results:
192+
if state_full:
193+
filtered = [
194+
result
195+
for result in results
196+
if (result.get("admin1") or "").lower()
197+
== state_full.lower()
198+
]
199+
if filtered:
200+
results = filtered
201+
match = results[0]
202+
for result in results:
203+
name = result.get("name")
204+
admin1 = result.get("admin1")
205+
country = result.get("country")
206+
formatted = ", ".join(
207+
part for part in [name, admin1, country] if part
208+
)
209+
if formatted and formatted not in suggested_queries:
210+
suggested_queries.append(formatted)
211+
break
212+
except Exception as exc:
213+
last_error = exc
214+
215+
if match is None:
216+
if last_error:
217+
return {
218+
"status": "error",
219+
"message": f"Sorry, I couldn't look up the location \"{raw_location}\" right now.",
220+
"suggestedQueries": suggested_queries,
221+
}
222+
return {
223+
"status": "not_found",
224+
"message": f"Sorry, I couldn't find a location match for \"{raw_location}\".",
225+
"suggestedQueries": suggested_queries,
226+
}
227+
228+
latitude = match.get("latitude")
229+
longitude = match.get("longitude")
230+
name = match.get("name") or raw_location
231+
admin1 = match.get("admin1")
232+
country = match.get("country")
233+
place = ", ".join(part for part in [name, admin1, country] if part)
234+
235+
forecast_params = urllib.parse.urlencode(
236+
{
237+
"latitude": latitude,
238+
"longitude": longitude,
239+
"current": "temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code",
240+
"temperature_unit": "fahrenheit",
241+
"windspeed_unit": "mph",
242+
}
243+
)
244+
forecast_url = f"https://api.open-meteo.com/v1/forecast?{forecast_params}"
245+
246+
try:
247+
with urllib.request.urlopen(forecast_url, timeout=10) as response:
248+
forecast = json.loads(response.read().decode("utf-8"))
249+
except Exception:
250+
return {
251+
"status": "error",
252+
"message": f"Sorry, I couldn't fetch the weather for {place} right now.",
253+
"suggestedQueries": suggested_queries,
254+
}
255+
256+
current = forecast.get("current") or {}
257+
temperature = current.get("temperature_2m")
258+
feels_like = current.get("apparent_temperature")
259+
humidity = current.get("relative_humidity_2m")
260+
windspeed = current.get("wind_speed_10m")
261+
weather_code = current.get("weather_code")
262+
263+
if temperature is None:
264+
return {
265+
"status": "error",
266+
"message": f"Sorry, I couldn't read the current weather for {place}.",
267+
"suggestedQueries": suggested_queries,
268+
}
269+
270+
details = []
271+
if feels_like is not None:
272+
details.append(f"feels like {feels_like}°F")
273+
if humidity is not None:
274+
details.append(f"humidity {humidity}%")
275+
if windspeed is not None:
276+
details.append(f"wind {windspeed} mph")
277+
if weather_code is not None:
278+
details.append(f"code {weather_code}")
279+
extra = f" ({', '.join(details)})" if details else ""
280+
281+
return {
282+
"status": "ok",
283+
"location": place,
284+
"temperatureF": temperature,
285+
"feelsLikeF": feels_like,
286+
"humidityPercent": humidity,
287+
"summary": f"The weather for {place} is {temperature}°F{extra}.",
288+
"suggestedQueries": suggested_queries,
289+
}
60290

61291
class AgentState(CopilotKitState):
62292
proverbs: List[str]
@@ -73,7 +303,14 @@ class AgentContext(TypedDict, total=False):
73303
middleware=[CopilotKitMiddleware(), apply_structured_output_schema],
74304
context_schema=AgentContext,
75305
state_schema=AgentState,
76-
system_prompt="You are a helpful research assistant."
306+
system_prompt=(
307+
"You are a weather reporter. The user will ask questions about the weather.\n"
308+
"Use the get_weather tool for specific locations, then create a full report.\n"
309+
"Call the tool with a specific place in the format: City, State, Country.\n"
310+
"If the user gives a broad region (e.g., 'North Alabama'), choose 2-4\n"
311+
"representative cities in that region and fetch each via get_weather.\n"
312+
"Present a multi-city report."
313+
),
77314
)
78315

79316
graph = agent

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"next": "16.1.1",
2323
"react": "^19.2.3",
2424
"react-dom": "^19.2.3",
25-
"react18-json-view": "^0.2.9"
25+
"react18-json-view": "^0.2.9",
26+
"streamdown": "^2.1.0"
2627
},
2728
"devDependencies": {
2829
"@eslint/eslintrc": "^3",

src/app/parser/page.tsx

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ const jsonDocument = `{
2626
]
2727
}`;
2828

29-
const schema = s.object("demo-json", {
30-
title: s.string("Title"),
29+
const schema = s.streaming.object("demo-json", {
30+
title: s.streaming.string("Title"),
3131
version: s.number("Version"),
3232
status: s.string("Status"),
33-
features: s.array("Features", s.string("Feature")),
33+
features: s.streaming.array("Features", s.streaming.string("Feature")),
3434
theme: s.object("Theme", {
3535
primary: s.string("Primary color"),
3636
secondary: s.string("Secondary color"),
@@ -43,9 +43,9 @@ const schema = s.object("demo-json", {
4343
}),
4444
contributors: s.array(
4545
"Contributors",
46-
s.object("Contributor", {
47-
name: s.string("Name"),
48-
role: s.string("Role"),
46+
s.streaming.object("Contributor", {
47+
name: s.streaming.string("Name"),
48+
role: s.streaming.string("Role"),
4949
active: s.boolean("Active"),
5050
}),
5151
),
@@ -97,7 +97,9 @@ function getNodeValueLabel(node: JsonAstNode) {
9797
case "number":
9898
return node.resolvedValue ?? node.buffer ?? "";
9999
case "boolean":
100-
return node.resolvedValue === undefined ? "…" : String(node.resolvedValue);
100+
return node.resolvedValue === undefined
101+
? "…"
102+
: String(node.resolvedValue);
101103
case "null":
102104
return node.resolvedValue === null ? "null" : "…";
103105
case "array":
@@ -272,7 +274,10 @@ function AstVisualizer({ parserState }: { parserState: ParserState }) {
272274
const spacingY = 140;
273275

274276
return (
275-
<div className="relative" style={{ width: layout.width, height: layout.height }}>
277+
<div
278+
className="relative"
279+
style={{ width: layout.width, height: layout.height }}
280+
>
276281
<svg
277282
className="absolute inset-0"
278283
width={layout.width}
@@ -352,7 +357,7 @@ function AstVisualizer({ parserState }: { parserState: ParserState }) {
352357
export default function ParserVisualizerPage() {
353358
const [cursor, setCursor] = useState(jsonDocument.length);
354359
const jsonSlice = jsonDocument.slice(0, cursor);
355-
const { parserState } = useJsonParser(jsonSlice, schema);
360+
const { parserState, value } = useJsonParser(jsonSlice, schema);
356361

357362
const ticks = useMemo(() => Array.from({ length: jsonDocument.length }), []);
358363

@@ -378,20 +383,32 @@ export default function ParserVisualizerPage() {
378383
</div>
379384
</header>
380385

381-
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[1fr_1.2fr]">
386+
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,1fr)_720px]">
382387
<section className="flex min-h-0 flex-col border-b border-slate-800 bg-slate-900/40 md:border-b-0 md:border-r">
383-
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
384-
JSON Document
388+
<div className="flex min-h-0 flex-1 flex-col">
389+
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
390+
JSON Document
391+
</div>
392+
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
393+
<pre className="whitespace-pre-wrap text-sm leading-6">
394+
<span className="text-emerald-200">
395+
{jsonDocument.slice(0, cursor)}
396+
</span>
397+
<span className="text-slate-600">
398+
{jsonDocument.slice(cursor)}
399+
</span>
400+
</pre>
401+
</div>
385402
</div>
386-
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
387-
<pre className="whitespace-pre-wrap text-sm leading-6">
388-
<span className="text-emerald-200">
389-
{jsonDocument.slice(0, cursor)}
390-
</span>
391-
<span className="text-slate-600">
392-
{jsonDocument.slice(cursor)}
393-
</span>
394-
</pre>
403+
<div className="flex min-h-0 flex-[0.6] flex-col border-t border-slate-800">
404+
<div className="border-b border-slate-800 px-6 py-3 text-xs uppercase tracking-[0.3em] text-slate-400">
405+
Resolved Value
406+
</div>
407+
<div className="min-h-0 flex-1 overflow-auto px-6 py-4">
408+
<pre className="whitespace-pre-wrap text-sm leading-6 text-slate-200">
409+
{value ? JSON.stringify(value, null, 2) : "—"}
410+
</pre>
411+
</div>
395412
</div>
396413
</section>
397414

@@ -403,8 +420,8 @@ export default function ParserVisualizerPage() {
403420
<AstVisualizer parserState={parserState} />
404421
{parserState.error ? (
405422
<div className="mt-4 rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-xs text-rose-200">
406-
{parserState.error.message} (line {parserState.error.line},
407-
col {parserState.error.column})
423+
{parserState.error.message} (line {parserState.error.line}, col{" "}
424+
{parserState.error.column})
408425
</div>
409426
) : null}
410427
</div>

0 commit comments

Comments
 (0)