From e720768134cdd19157e18e7ac3f3acc00a937609 Mon Sep 17 00:00:00 2001 From: JobRamos <33988720+JobRamos@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:27:28 -0600 Subject: [PATCH] Add Weather Agent use case with harness, gateway, guardrails, evaluation and observability (#1648) * Add Weather Agent use case with harness, gateway, guardrails, evaluations, and observability * fix: resolve lint errors and security finding (unused imports, f-strings, bind to localhost) * fix: address PR feedback (README clarifications, region detection, graceful eval error, trace display) * feat: add Skills (xlsx report generation) and Optimization (system prompt recommendations) * feat: add Skills (xlsx report generation) and Optimization (system prompt recommendations) * fix: address PR feedback (README clarifications, region detection, graceful eval error, trace display) --- .../02-use-cases/04-weather-agent/.gitignore | 11 + .../02-use-cases/04-weather-agent/README.md | 179 ++ .../04-weather-agent/backend/agent.py | 61 + .../04-weather-agent/backend/evaluation.py | 147 ++ .../04-weather-agent/backend/main.py | 163 ++ .../04-weather-agent/backend/observability.py | 71 + .../04-weather-agent/backend/optimization.py | 116 ++ .../04-weather-agent/backend/requirements.txt | 7 + .../04-weather-agent/backend/resources.py | 247 +++ .../04-weather-agent/backend/skills.py | 212 ++ .../02-use-cases/04-weather-agent/cleanup.sh | 65 + .../04-weather-agent/frontend/index.html | 12 + .../frontend/package-lock.json | 1795 +++++++++++++++++ .../04-weather-agent/frontend/package.json | 18 + .../04-weather-agent/frontend/src/App.css | 427 ++++ .../04-weather-agent/frontend/src/App.jsx | 548 +++++ .../04-weather-agent/frontend/src/api.js | 69 + .../04-weather-agent/frontend/src/main.jsx | 10 + .../04-weather-agent/frontend/vite.config.js | 13 + .../04-weather-agent/images/app_example.png | Bin 0 -> 239047 bytes .../02-use-cases/04-weather-agent/optimize.py | 284 +++ .../02-use-cases/04-weather-agent/run.sh | 145 ++ .../02-use-cases/04-weather-agent/start.sh | 172 ++ .../04-weather-agent/weather_agent.py | 513 +++++ 01-features/01-harness/README.md | 3 + 25 files changed, 5288 insertions(+) create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/.gitignore create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/README.md create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/agent.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/evaluation.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/main.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/observability.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/optimization.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/requirements.txt create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/resources.py create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/backend/skills.py create mode 100755 01-features/01-harness/02-use-cases/04-weather-agent/cleanup.sh create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/index.html create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/package-lock.json create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/package.json create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/src/App.css create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/src/App.jsx create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/src/api.js create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/src/main.jsx create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/frontend/vite.config.js create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/images/app_example.png create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/optimize.py create mode 100755 01-features/01-harness/02-use-cases/04-weather-agent/run.sh create mode 100755 01-features/01-harness/02-use-cases/04-weather-agent/start.sh create mode 100644 01-features/01-harness/02-use-cases/04-weather-agent/weather_agent.py diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/.gitignore b/01-features/01-harness/02-use-cases/04-weather-agent/.gitignore new file mode 100644 index 00000000..00582303 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/.gitignore @@ -0,0 +1,11 @@ +venv/ +node_modules/ +frontend/node_modules/ +resource_info.json +optimization_result.json +backend.log +frontend.log +backend.pid +frontend.pid +pr-response.txt +__pycache__/ diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/README.md b/01-features/01-harness/02-use-cases/04-weather-agent/README.md new file mode 100644 index 00000000..0fdf694f --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/README.md @@ -0,0 +1,179 @@ +# Weather Agent — Harness + Evaluations + Gateway + Observability + + + +## Overview + +A full-stack weather agent web app that integrates **six AgentCore capabilities** in a single demo: + +1. **AgentCore Gateway** — Creates a Gateway resource with an Exa MCP target, routing all tool calls through the managed proxy for centralized observability +2. **Guardrails** — Bedrock guardrail that anonymizes PII (email, phone, address) in agent responses +3. **Observability** — CloudWatch traces with full agent loop visibility +4. **Skills** — Generate weather forecast Excel spreadsheets using the xlsx skill (fetched from Git at invocation time) +5. **Evaluations** — Batch evaluation scoring with built-in evaluators (Helpfulness, Correctness, Coherence, etc.) +6. **Optimization** — AI-generated system prompt recommendations based on agent traces + +The web app features: +- A **chat interface** where users ask weather questions +- **Weather data cards** that update in real time (temperature, wind, UV, sunrise/sunset) +- A **Traces panel** showing live trace IDs from CloudWatch (searchable in GenAI Observability) +- A **Skills panel** to generate weather forecast XLSX reports +- An **Evaluations panel** that triggers batch evaluations and displays scores +- An **Optimization panel** that generates AI-improved system prompts from your traces + +## Quick Start + +```bash +./start.sh +``` + +One command: installs dependencies, provisions AWS resources (Gateway, Harness, Guardrail), starts the backend and frontend. Open **http://localhost:5173**. + +To stop servers: `Ctrl+C`. To delete AWS resources: `./cleanup.sh`. + +## Architecture + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Frontend (React + Vite) — http://localhost:5173 │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Chat Panel │ │ Weather / Traces / Evaluations │ │ +│ │ (send queries) │ │ (live cards, trace IDs, scores)│ │ +│ └──────────┬───────────┘ └────────────────┬────────────────┘ │ +└─────────────┼─────────────────────────────────┼──────────────────┘ + │ │ + │ POST /api/chat (SSE) │ GET /api/traces + │ │ POST /api/evaluate + ▼ ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ Backend (FastAPI) — http://localhost:8000 │ +│ │ +│ ┌────────────┐ ┌────────┐ ┌──────────────┐ ┌─────────────┐ │ +│ │ resources │ │ agent │ │observability │ │ evaluation │ │ +│ │ .py │ │ .py │ │ .py │ │ .py │ │ +│ └─────┬──────┘ └───┬────┘ └──────┬───────┘ └──────┬──────┘ │ +└────────┼──────────────┼──────────────┼─────────────────┼─────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ AWS (AgentCore + Bedrock + CloudWatch) │ +│ │ +│ AC Gateway ──► Exa MCP ──► Web Search (live weather data) │ +│ Harness ─────► Claude Haiku 4.5 (agent orchestration) │ +│ Guardrail ───► PII anonymization (email, phone, address) │ +│ Skills ──────► xlsx skill (Git-fetched, weather report gen) │ +│ CloudWatch ──► Trace observability (GenAI Observability) │ +│ Batch Eval ──► Built-in evaluators (Helpfulness, Correctness…) │ +│ Optimization ► System prompt recommendations from traces │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## How It Works + +### Web App Flow + +1. **Start** — `./start.sh` provisions Gateway + Harness + Guardrail (or reuses existing ones) +2. **Chat** — User asks weather questions; agent searches via Gateway's Exa MCP target +3. **Weather Cards** — Parsed metrics (temperature, wind, UV, etc.) appear as visual cards +4. **Traces** — Each invocation generates traces visible in the Traces tab and in CloudWatch > GenAI Observability > Bedrock AgentCore > Traces +5. **Skills** — Click "Generate Report" to create an XLSX weather forecast using the xlsx skill +6. **Evaluations** — Click "Run Eval" to trigger a batch evaluation; results show scores for Helpfulness, Correctness, Coherence, and more (also visible in Bedrock AgentCore > Evaluations > Batch evaluation) +7. **Optimization** — Click "Optimize" to generate an AI-improved system prompt from your traces (also visible in Bedrock AgentCore > Optimizations > Recommendations) +8. **Cleanup** — `./cleanup.sh` deletes all AWS resources including batch evaluations + + +## Key Features + +### AgentCore Gateway +The demo creates an AgentCore Gateway resource (`create_gateway` + `create_gateway_target`) and passes it to the harness as `type: "agentcore_gateway"`. The Gateway acts as a managed proxy between the agent and external tool servers: +- Centralized routing for MCP tool traffic +- Automatic observability (every tool call through the Gateway is traced) +- Configurable auth (NONE in this demo, supports IAM/OAuth) + +### Bedrock Guardrails +A guardrail anonymizes PII in agent responses. If you ask the agent to include personal info (email, phone), the guardrail masks it before the response reaches you. + +### Observability +Every `invoke_harness` call automatically generates traces in CloudWatch. The Traces tab shows trace IDs that you can search in: +- **CloudWatch > GenAI Observability > Bedrock AgentCore > Traces** + +### Skills (xlsx) +The "Generate Report" button creates a 7-day weather forecast Excel spreadsheet using the AgentCore xlsx skill. The skill is fetched from Git (`https://github.com/anthropics/skills`) at invocation time — no container setup or pre-installation required. The report uses the last city you asked about. + +### Batch Evaluations +The "Run Eval" button triggers a batch evaluation that scores your session using built-in evaluators: +- InstructionFollowing, Helpfulness, Correctness, Faithfulness, ResponseRelevance, Coherence, Conciseness, Refusal + +Results appear in the web app and are also visible in: +- **Bedrock AgentCore > Evaluations > Batch evaluation** + +### Optimization +The "Optimize" button analyzes your agent's traces and generates an AI-improved system prompt optimized for goal success. It uses the `start_recommendation` API with your harness traces as input. The recommended prompt and explanation are displayed in the web app. + +Results are also visible in: +- **Bedrock AgentCore > Optimizations > Recommendations** + +## Prerequisites + +- Python 3.10+ +- Node.js 18+ +- AWS CLI configured with credentials (`aws sts get-caller-identity` should work). Recommended region: **us-east-1** (`export AWS_DEFAULT_REGION=us-east-1`) +- Model access enabled for Claude Haiku 4.5 in Amazon Bedrock +- [CloudWatch Transaction Search](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html) enabled — **required** for Traces, Evaluations, and Optimization to work. After enabling, wait 10-15 minutes before using these feature so the Bedrock AgentCore dashboard in CloudWatch can become available. Only traces from invocations *after* enabling will be indexed. + +## AWS Permissions Required + +> **Note:** The policies below use broad access for simplicity in this demo. In production environments, follow the principle of least privilege and create custom IAM policies scoped to only the specific resources and actions your agent needs. + +| Policy | Purpose | +|--------|---------| +| `BedrockAgentCoreFullAccess` | Harness, Gateway, Batch Evaluations | +| `AmazonBedrockFullAccess` | Model invocation, Guardrails | +| `IAMFullAccess` | Create the harness execution role (first run only) | +| `CloudWatchFullAccessV2` | Query traces + batch evaluation output logs | + +## Running + +### Web App (recommended) + +```bash +./start.sh +``` + +One command: creates a virtual environment, installs Python and Node.js packages, provisions AWS resources, starts the FastAPI backend and React frontend. + +Open **http://localhost:5173** once the script prints "App is running!". + +```bash +# Stop servers without deleting AWS resources: +# Press Ctrl+C (resources persist for next ./start.sh) + +# Stop servers AND delete all AWS resources: +./cleanup.sh +``` + +## Sample Prompts + +- "What's the weather in Tokyo?" +- "What's the wind speed in Vancouver right now?" +- "What's the UV index in Miami today?" +- "When is sunrise and sunset in London?" + + + +## Clean Up + +```bash +# Delete all AWS resources (gateway, harness, guardrail, batch evaluations, IAM role): +./cleanup.sh + +# To also remove the virtual environment and node_modules: +rm -rf venv frontend/node_modules +``` \ No newline at end of file diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/agent.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/agent.py new file mode 100644 index 00000000..9873ed15 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/agent.py @@ -0,0 +1,61 @@ +"""Agent invocation — streaming wrapper around invoke_harness.""" + +import sys +from pathlib import Path +from typing import Generator + +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) +from utils.client import get_agentcore_client + +MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0" + +SYSTEM_PROMPT = ( + "You are a weather assistant. You ONLY answer questions about weather, " + "climate, and atmospheric conditions (temperature, wind, humidity, UV index, " + "sunrise, sunset, moon phase, forecasts, air quality, precipitation). " + "If the user asks about anything unrelated to weather, politely redirect them. " + "For example: 'I'm a weather assistant — I can help with forecasts, current conditions, " + "UV index, wind, sunrise/sunset, and more. What location would you like weather for?'\n\n" + "When answering weather questions:\n" + "- Always search for real-time data using your tools\n" + "- Include specific numbers with units (temperature in °F/°C, wind in km/h or mph)\n" + "- Mention the city name in your response\n" + "- Keep responses concise and well-structured" +) + + +def invoke_agent(harness_arn: str, gateway_arn: str, session_id: str, message: str) -> Generator[dict, None, None]: + """Stream agent response as SSE-friendly dicts.""" + client = get_agentcore_client() + + tools = [ + { + "type": "agentcore_gateway", + "name": "gateway", + "config": {"agentCoreGateway": {"gatewayArn": gateway_arn}}, + } + ] + + prefixed_message = f"[INSTRUCTIONS: {SYSTEM_PROMPT}]\n\nUser question: {message}" + + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + messages=[{"role": "user", "content": [{"text": prefixed_message}]}], + model={"bedrockModelConfig": {"modelId": MODEL_ID}}, + tools=tools, + ) + + for event in response["stream"]: + if "contentBlockStart" in event: + start = event["contentBlockStart"].get("start", {}) + if "toolUse" in start: + yield {"type": "tool", "name": start["toolUse"].get("name", "?")} + elif "contentBlockDelta" in event: + delta = event["contentBlockDelta"].get("delta", {}) + if "text" in delta: + yield {"type": "text", "content": delta["text"]} + elif "messageStop" in event: + yield {"type": "done"} + elif "internalServerException" in event: + yield {"type": "error", "content": str(event["internalServerException"])} diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/evaluation.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/evaluation.py new file mode 100644 index 00000000..dbd26af3 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/evaluation.py @@ -0,0 +1,147 @@ +"""Evaluations — run batch evaluation against harness session traces.""" + +import time +import uuid + +import boto3 + +from resources import REGION + +EVALUATOR_IDS = [ + "Builtin.InstructionFollowing", + "Builtin.Helpfulness", + "Builtin.Correctness", + "Builtin.Faithfulness", + "Builtin.ResponseRelevance", + "Builtin.Coherence", + "Builtin.Conciseness", + "Builtin.Refusal", +] + + +def _discover_log_group(harness_name: str) -> str | None: + """Find the CloudWatch log group for a harness by prefix search.""" + logs = boto3.client("logs", region_name=REGION) + prefix = f"/aws/bedrock-agentcore/runtimes/harness_{harness_name}-" + resp = logs.describe_log_groups(logGroupNamePrefix=prefix, limit=5) + groups = resp.get("logGroups", []) + if groups: + # Return the most recently created one + groups.sort(key=lambda g: g.get("creationTime", 0), reverse=True) + return groups[0]["logGroupName"] + return None + + +def run_batch_evaluation(harness_id: str, harness_name: str = None) -> dict: + """Start a batch evaluation job and poll until complete. Returns results.""" + client = boto3.client("bedrock-agentcore", region_name=REGION) + + if not hasattr(client, "start_batch_evaluation"): + return { + "error": ( + "start_batch_evaluation is not available in your boto3 version. " + "Please upgrade: pip install 'boto3>=1.43.27'" + ), + "scores": [], + } + + # Discover the log group dynamically (it has a random suffix) + log_group = None + if harness_name: + log_group = _discover_log_group(harness_name) + if not log_group: + # Fallback: try with harness_id directly + log_group = _discover_log_group(harness_id) + if not log_group: + return {"error": f"Could not find log group for harness {harness_name or harness_id}", "scores": []} + + # Service name format: harness_{name}.DEFAULT (without the random suffix) + # Log group: harness_WeatherAgent_537bb0c9-d9RslKDml1-DEFAULT + # Service: harness_WeatherAgent_537bb0c9.DEFAULT + log_group_basename = log_group.split("/")[-1] # harness_WeatherAgent_537bb0c9-d9RslKDml1-DEFAULT + parts = log_group_basename.rsplit("-", 2) # ['harness_WeatherAgent_537bb0c9', 'd9RslKDml1', 'DEFAULT'] + service_name = f"{parts[0]}.DEFAULT" if len(parts) >= 3 else log_group_basename.replace("-DEFAULT", ".DEFAULT") + + batch_name = f"weather_eval_{uuid.uuid4().hex[:8]}" + + try: + resp = client.start_batch_evaluation( + batchEvaluationName=batch_name, + evaluators=[{"evaluatorId": eid} for eid in EVALUATOR_IDS], + dataSourceConfig={ + "cloudWatchLogs": { + "serviceNames": [service_name], + "logGroupNames": [log_group], + } + }, + ) + except Exception as e: + return {"error": str(e), "scores": []} + + batch_id = resp["batchEvaluationId"] + + # Poll until complete (timeout after 5 minutes) + deadline = time.monotonic() + 300 + status = "PENDING" + while time.monotonic() < deadline: + time.sleep(10) + try: + result = client.get_batch_evaluation(batchEvaluationId=batch_id) + status = result.get("status", "UNKNOWN") + if status in ("COMPLETED", "COMPLETED_WITH_ERRORS", "FAILED"): + break + except Exception: + pass + + if status not in ("COMPLETED", "COMPLETED_WITH_ERRORS"): + # Try to get failure details + failure_reason = "" + try: + failure_reason = result.get("failureReasons", result.get("statusReason", "")) + if not failure_reason: + # Check evaluationResults for per-session errors + eval_res = result.get("evaluationResults", {}) + failed_count = eval_res.get("numberOfSessionsFailed", 0) + completed_count = eval_res.get("numberOfSessionsCompleted", 0) + if failed_count > 0: + failure_reason = f"{failed_count} session(s) failed, {completed_count} completed" + except Exception: + pass + error_msg = f"Evaluation did not complete (status: {status})" + if failure_reason: + error_msg += f". {failure_reason}" + print(f"[eval] Failed: {error_msg}") + print(f"[eval] Full response: {result}") + return { + "batch_id": batch_id, + "batch_name": batch_name, + "status": status, + "error": error_msg, + "scores": [], + } + + # Extract evaluator results + scores = [] + eval_results = result.get("evaluationResults", {}) + summaries = eval_results.get("evaluatorSummaries", []) + + for summary in summaries: + eid = summary.get("evaluatorId", "") + stats = summary.get("statistics", {}) + avg_score = stats.get("averageScore") + evaluated = summary.get("totalEvaluated", 0) + + name = eid.replace("Builtin.", "") if eid.startswith("Builtin.") else eid + scores.append({ + "evaluator": name, + "score": avg_score, + "evaluated_sessions": evaluated, + }) + + return { + "batch_id": batch_id, + "batch_name": batch_name, + "status": status, + "total_sessions": eval_results.get("numberOfSessionsCompleted", 0), + "scores": scores, + } diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/main.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/main.py new file mode 100644 index 00000000..297112e2 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/main.py @@ -0,0 +1,163 @@ +"""FastAPI backend for the Weather Agent web app.""" + +import asyncio +import json +import uuid +from contextlib import asynccontextmanager + +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from sse_starlette.sse import EventSourceResponse + +from resources import ensure_resources +from agent import invoke_agent +from observability import get_recent_traces, get_transaction_search_status +from evaluation import run_batch_evaluation +from skills import generate_weather_report +from optimization import run_optimization + +# Global state +_state: dict = {} +_sessions: dict[str, list] = {} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global _state + print("[backend] Starting — provisioning AWS resources...") + _state = await asyncio.to_thread(ensure_resources) + print(f"[backend] Ready. Harness: {_state['harness_id']}") + yield + print("[backend] Shutting down") + + +app = FastAPI(title="Weather Agent", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://localhost:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +class ChatRequest(BaseModel): + message: str + session_id: str | None = None + + +class EvalRequest(BaseModel): + session_id: str + + +class ReportRequest(BaseModel): + session_id: str + city: str | None = None + + +@app.get("/health") +async def health(): + return {"status": "ok", "harness_id": _state.get("harness_id")} + + +@app.get("/api/status") +async def status(): + return { + "ready": bool(_state.get("harness_id")), + "harness_id": _state.get("harness_id"), + "harness_name": _state.get("harness_name"), + "gateway_id": _state.get("gateway_id"), + "gateway_name": _state.get("gateway_name"), + "guardrail_id": _state.get("guardrail_id"), + "guardrail_name": _state.get("guardrail_name"), + "region": _state.get("region"), + } + + +@app.post("/api/chat") +async def chat(req: ChatRequest): + if not _state.get("harness_arn"): + raise HTTPException(503, "Resources not ready") + + session_id = req.session_id or str(uuid.uuid4()).upper() + + if session_id not in _sessions: + _sessions[session_id] = [] + + _sessions[session_id].append({"role": "user", "content": req.message}) + + async def generate(): + full_text = "" + yield json.dumps({"type": "session_id", "session_id": session_id}) + + for event in invoke_agent( + _state["harness_arn"], _state["gateway_arn"], session_id, req.message + ): + if event["type"] == "text": + full_text += event["content"] + yield json.dumps(event) + + _sessions[session_id].append({"role": "assistant", "content": full_text}) + + return EventSourceResponse(generate(), media_type="text/event-stream") + + +@app.get("/api/traces") +async def traces(minutes: int = 10): + result = await asyncio.to_thread(get_recent_traces, _state.get("harness_name"), minutes) + tx_status = await asyncio.to_thread(get_transaction_search_status) + return {"traces": result, "transaction_search": tx_status} + + +@app.post("/api/evaluate") +async def evaluate(req: EvalRequest): + if not _state.get("harness_id"): + raise HTTPException(503, "Resources not ready") + + result = await asyncio.to_thread( + run_batch_evaluation, + _state["harness_id"], + _state.get("harness_name"), + ) + return {"session_id": req.session_id, **result} + + +@app.post("/api/generate-report") +async def generate_report(req: ReportRequest): + if not _state.get("harness_arn"): + raise HTTPException(503, "Resources not ready") + + result = await asyncio.to_thread( + generate_weather_report, + _state["harness_arn"], + _state["harness_id"], + req.session_id, + req.city or "the cities discussed", + ) + return result + + +@app.post("/api/optimize") +async def optimize(): + if not _state.get("harness_name"): + raise HTTPException(503, "Resources not ready") + + result = await asyncio.to_thread( + run_optimization, _state["harness_name"] + ) + return result + + +@app.get("/api/sessions") +async def sessions(): + return { + sid: {"turns": len(msgs), "last_message": msgs[-1]["content"][:80] if msgs else ""} + for sid, msgs in _sessions.items() + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/observability.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/observability.py new file mode 100644 index 00000000..12e3e6bc --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/observability.py @@ -0,0 +1,71 @@ +"""Observability — query traces from the aws/spans CloudWatch log group.""" + +import time + +import boto3 + +from resources import REGION + +SPANS_LOG_GROUP = "aws/spans" + + +def get_recent_traces(harness_name: str = None, minutes: int = 10) -> list[dict]: + """Query aws/spans log group for recent traces from this harness.""" + logs = boto3.client("logs", region_name=REGION) + + end_time = int(time.time()) + start_time = end_time - (minutes * 60) + + query = """fields traceId, @timestamp +| filter ispresent(traceId) and traceId != '' +| stats count() as spans by traceId +| sort @timestamp desc +| limit 20""" + + try: + resp = logs.start_query( + logGroupName=SPANS_LOG_GROUP, + startTime=start_time, + endTime=end_time, + queryString=query, + ) + query_id = resp["queryId"] + + for _ in range(15): + time.sleep(2) + result = logs.get_query_results(queryId=query_id) + if result["status"] in ("Complete", "Failed", "Cancelled"): + break + + if result["status"] != "Complete": + return [] + + traces = [] + for row in result.get("results", []): + fields = {f["field"]: f["value"] for f in row} + trace_id = fields.get("traceId", "") + spans = fields.get("spans", "0") + + if trace_id: + traces.append({ + "trace_id": trace_id, + "spans": int(spans), + "has_error": False, + "has_fault": False, + }) + + return traces + + except Exception as e: + return [{"error": str(e)}] + + +def get_transaction_search_status() -> dict: + """Check if Transaction Search is enabled.""" + xray = boto3.client("xray", region_name=REGION) + try: + rules = xray.get_indexing_rules() + sampling = rules["IndexingRules"][0]["Rule"]["Probabilistic"]["DesiredSamplingPercentage"] + return {"enabled": True, "sampling_percentage": sampling} + except Exception as e: + return {"enabled": False, "error": str(e)} diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/optimization.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/optimization.py new file mode 100644 index 00000000..367726fa --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/optimization.py @@ -0,0 +1,116 @@ +"""Optimization — generate system prompt recommendations from agent traces.""" + +import time +import uuid +from datetime import datetime, timedelta, timezone + +import boto3 + +from resources import REGION +from agent import SYSTEM_PROMPT + + +def _discover_log_group_arn(harness_name: str) -> tuple[str, str] | None: + """Find the log group ARN and service name for a harness.""" + logs = boto3.client("logs", region_name=REGION) + prefix = f"/aws/bedrock-agentcore/runtimes/harness_{harness_name}-" + resp = logs.describe_log_groups(logGroupNamePrefix=prefix, limit=5) + groups = resp.get("logGroups", []) + if not groups: + return None + groups.sort(key=lambda g: g.get("creationTime", 0), reverse=True) + log_group = groups[0] + log_group_arn = log_group["arn"] + log_group_name = log_group["logGroupName"] + + basename = log_group_name.split("/")[-1] + parts = basename.rsplit("-", 2) + service_name = f"{parts[0]}.DEFAULT" if len(parts) >= 3 else basename.replace("-DEFAULT", ".DEFAULT") + + return log_group_arn, service_name + + +def run_optimization(harness_name: str, evaluator: str = "Builtin.GoalSuccessRate") -> dict: + """Run a system prompt recommendation and return the result.""" + client = boto3.client("bedrock-agentcore", region_name=REGION) + + # Discover log group + result = _discover_log_group_arn(harness_name) + if not result: + return {"error": "Could not find log group. Send some chat messages first.", "status": "FAILED"} + + log_group_arn, service_name = result + + now = datetime.now(timezone.utc) + start_time = now - timedelta(days=7) + rec_name = f"weather_rec_{uuid.uuid4().hex[:8]}" + + # Start recommendation + try: + resp = client.start_recommendation( + name=rec_name, + type="SYSTEM_PROMPT_RECOMMENDATION", + recommendationConfig={ + "systemPromptRecommendationConfig": { + "systemPrompt": {"text": SYSTEM_PROMPT}, + "agentTraces": { + "cloudwatchLogs": { + "logGroupArns": [log_group_arn], + "serviceNames": [service_name], + "startTime": start_time, + "endTime": now, + } + }, + "evaluationConfig": { + "evaluators": [ + {"evaluatorArn": f"arn:aws:bedrock-agentcore:::evaluator/{evaluator}"} + ] + }, + } + }, + clientToken=str(uuid.uuid4()), + ) + except Exception as e: + return {"error": str(e), "status": "FAILED"} + + rec_id = resp["recommendationId"] + + # Poll for completion (timeout 5 minutes) + status = "PENDING" + for _ in range(30): + time.sleep(10) + try: + rec = client.get_recommendation(recommendationId=rec_id) + status = rec.get("status", "UNKNOWN") + if status in ("COMPLETED", "FAILED"): + break + except Exception: + pass + + if status != "COMPLETED": + error_msg = "" + if status == "FAILED": + rec_result = rec.get("recommendationResult", {}).get( + "systemPromptRecommendationResult", {} + ) + error_msg = rec_result.get("errorMessage", "Unknown error") + return { + "status": status, + "error": error_msg or f"Recommendation did not complete (status: {status})", + "recommendation_name": rec_name, + } + + # Extract result + rec_result = rec.get("recommendationResult", {}).get( + "systemPromptRecommendationResult", {} + ) + + return { + "status": "COMPLETED", + "recommendation_name": rec_name, + "recommendation_id": rec_id, + "evaluator": evaluator, + "current_prompt": SYSTEM_PROMPT, + "recommended_prompt": rec_result.get("recommendedSystemPrompt", ""), + "explanation": rec_result.get("explanation", ""), + } diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/requirements.txt b/01-features/01-harness/02-use-cases/04-weather-agent/backend/requirements.txt new file mode 100644 index 00000000..9c788f16 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/requirements.txt @@ -0,0 +1,7 @@ +boto3>=1.43.27 +fastapi +uvicorn[standard] +sse-starlette +pydantic +bedrock-agentcore +requests diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/resources.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/resources.py new file mode 100644 index 00000000..a3c5f6bf --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/resources.py @@ -0,0 +1,247 @@ +"""AWS resource lifecycle — create or reuse Gateway, Harness, Guardrail.""" + +import json +import os +import sys +import time +import uuid +from pathlib import Path + +import boto3 + +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) +from utils.iam import create_harness_role, delete_harness_role +from utils.client import get_agentcore_control_client + +STATE_FILE = Path(__file__).parent.parent / "resource_info.json" +REGION = os.environ.get("AWS_DEFAULT_REGION") or boto3.session.Session().region_name or "us-east-1" + + +def _poll(get_fn, extract_fn, target="READY", timeout=300, interval=5): + deadline = time.monotonic() + timeout + while True: + resp = get_fn() + status = extract_fn(resp) + if status == target: + return resp + if status in ("FAILED", "CREATE_FAILED", "DELETE_FAILED"): + raise RuntimeError(f"Resource failed: {status}") + if time.monotonic() > deadline: + raise TimeoutError(f"Not {target} after {timeout}s") + time.sleep(interval) + + +def _load_state() -> dict | None: + if STATE_FILE.exists(): + return json.loads(STATE_FILE.read_text()) + return None + + +def _save_state(state: dict): + STATE_FILE.write_text(json.dumps(state, indent=2)) + + +def _resources_alive(state: dict) -> bool: + try: + control = get_agentcore_control_client() + h = control.get_harness(harnessId=state["harness_id"]) + if h["harness"]["status"] != "READY": + return False + gw = boto3.client("bedrock-agentcore-control", region_name=REGION) + g = gw.get_gateway(gatewayIdentifier=state["gateway_id"]) + if g["status"] != "READY": + return False + return True + except Exception: + return False + + +def ensure_resources() -> dict: + """Create or reuse all AWS resources. Returns resource dict.""" + existing = _load_state() + if existing and _resources_alive(existing): + print("[resources] Reusing existing resources") + return existing + + print("[resources] Provisioning new resources...") + control = get_agentcore_control_client() + gw_control = boto3.client("bedrock-agentcore-control", region_name=REGION) + bedrock = boto3.client("bedrock", region_name=REGION) + + # IAM role + role_arn = create_harness_role() + time.sleep(10) + + # Gateway + gateway_name = f"WeatherGW-{uuid.uuid4().hex[:8]}" + resp = gw_control.create_gateway( + name=gateway_name, roleArn=role_arn, protocolType="MCP", authorizerType="NONE" + ) + gateway_id = resp["gatewayId"] + gateway_arn = resp["gatewayArn"] + _poll( + lambda: gw_control.get_gateway(gatewayIdentifier=gateway_id), + lambda r: r["status"], + ) + + # MCP target (Exa search) + resp = gw_control.create_gateway_target( + gatewayIdentifier=gateway_id, + name="exa-weather", + targetConfiguration={"mcp": {"mcpServer": {"endpoint": "https://mcp.exa.ai/mcp"}}}, + ) + target_id = resp["targetId"] + _poll( + lambda: gw_control.get_gateway_target(gatewayIdentifier=gateway_id, targetId=target_id), + lambda r: r["status"], + ) + + # Harness + harness_name = f"WeatherAgent_{uuid.uuid4().hex[:8]}" + resp = control.create_harness( + harnessName=harness_name, + executionRoleArn=role_arn, + systemPrompt=[ + { + "text": ( + "You are a weather assistant. You ONLY answer questions about weather, " + "climate, and atmospheric conditions (temperature, wind, humidity, UV index, " + "sunrise, sunset, moon phase, forecasts, air quality, precipitation). " + "If the user asks about anything unrelated to weather, politely redirect them. " + "For example: 'I'm a weather assistant — I can help with forecasts, current conditions, " + "UV index, wind, sunrise/sunset, and more. What location would you like weather for?' " + "When answering weather questions: always search for real-time data using your tools, " + "include specific numbers with units (temperature in F/C, wind in km/h or mph), " + "mention the city name in your response, and keep responses concise and well-structured." + ) + } + ], + ) + harness_id = resp["harness"]["harnessId"] + harness_arn = resp["harness"]["arn"] + _poll( + lambda: control.get_harness(harnessId=harness_id), + lambda r: r["harness"]["status"], + ) + + # Guardrail + guardrail_id = None + guardrail_version = None + guardrail_name = None + try: + guardrail_name = f"weather-pii-{uuid.uuid4().hex[:6]}" + gr = bedrock.create_guardrail( + name=guardrail_name, + description="Anonymize PII in weather agent responses", + sensitiveInformationPolicyConfig={ + "piiEntitiesConfig": [ + {"type": "EMAIL", "action": "ANONYMIZE"}, + {"type": "PHONE", "action": "ANONYMIZE"}, + {"type": "ADDRESS", "action": "ANONYMIZE"}, + {"type": "US_SOCIAL_SECURITY_NUMBER", "action": "ANONYMIZE"}, + ] + }, + blockedInputMessaging="Content blocked.", + blockedOutputsMessaging="Content blocked.", + ) + guardrail_id = gr["guardrailId"] + gv = bedrock.create_guardrail_version(guardrailIdentifier=guardrail_id, description="v1") + guardrail_version = gv["version"] + except Exception as e: + print(f"[resources] Guardrail creation failed (non-critical): {e}") + + state = { + "gateway_id": gateway_id, + "gateway_arn": gateway_arn, + "gateway_name": gateway_name, + "target_id": target_id, + "harness_id": harness_id, + "harness_arn": harness_arn, + "harness_name": harness_name, + "guardrail_id": guardrail_id, + "guardrail_name": guardrail_name, + "guardrail_version": guardrail_version, + "role_arn": role_arn, + "region": REGION, + } + _save_state(state) + print("[resources] All resources ready") + return state + + +def destroy_resources(): + """Delete all resources and remove state file.""" + state = _load_state() + if not state: + print("[resources] No state file found") + return + + control = get_agentcore_control_client() + gw_control = boto3.client("bedrock-agentcore-control", region_name=REGION) + bedrock = boto3.client("bedrock", region_name=REGION) + + if state.get("harness_id"): + try: + control.delete_harness(harnessId=state["harness_id"]) + print(f" Deleted harness: {state['harness_id']}") + except Exception as e: + print(f" Warning: {e}") + + if state.get("gateway_id") and state.get("target_id"): + try: + gw_control.delete_gateway_target( + gatewayIdentifier=state["gateway_id"], targetId=state["target_id"] + ) + print(f" Deleted target: {state['target_id']}") + time.sleep(10) + except Exception as e: + print(f" Warning: {e}") + + if state.get("gateway_id"): + try: + gw_control.delete_gateway(gatewayIdentifier=state["gateway_id"]) + print(f" Deleted gateway: {state['gateway_id']}") + except Exception as e: + print(f" Warning: {e}") + + if state.get("guardrail_id"): + try: + bedrock.delete_guardrail(guardrailIdentifier=state["guardrail_id"]) + print(f" Deleted guardrail: {state['guardrail_id']}") + except Exception as e: + print(f" Warning: {e}") + + # Delete batch evaluations created by this app + dp_client = boto3.client("bedrock-agentcore", region_name=REGION) + try: + evals = dp_client.list_batch_evaluations() + for ev in evals.get("batchEvaluations", evals.get("items", [])): + ev_name = ev.get("batchEvaluationName", ev.get("name", "")) + ev_id = ev.get("batchEvaluationId", "") + if ev_name.startswith("weather_eval_"): + try: + dp_client.delete_batch_evaluation(batchEvaluationId=ev_id) + print(f" Deleted batch evaluation: {ev_name}") + except Exception: + pass + except Exception as e: + print(f" Warning (batch evals): {e}") + + # Delete recommendations created by this app + try: + recs = dp_client.list_recommendations() + for rec in recs.get("recommendationSummaries", recs.get("recommendations", recs.get("items", []))): + rec_name = rec.get("name", "") + rec_id = rec.get("recommendationId", "") + if rec_name.startswith("weather_rec_"): + try: + dp_client.delete_recommendation(recommendationId=rec_id) + print(f" Deleted recommendation: {rec_name}") + except Exception: + pass + except Exception as e: + print(f" Warning (recommendations): {e}") + + delete_harness_role() + STATE_FILE.unlink(missing_ok=True) + print("[resources] Cleanup complete") diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/backend/skills.py b/01-features/01-harness/02-use-cases/04-weather-agent/backend/skills.py new file mode 100644 index 00000000..f65c88d6 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/backend/skills.py @@ -0,0 +1,212 @@ +"""Skills — generate weather forecast reports as XLSX spreadsheets. + +Tries Git-based skill fetching first (no container needed). Falls back to +path-based approach with Node.js container if Git parameter is not supported. +""" + +import sys +import time +from pathlib import Path + +import boto3 +from botocore.config import Config +from botocore.exceptions import ParamValidationError + +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) +from utils.client import get_agentcore_control_client, get_agentcore_client + +from resources import REGION + +NODE_CONTAINER = "public.ecr.aws/docker/library/node:slim" +MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0" + +_skill_installed: dict[str, bool] = {} + + +def _run_command(client, harness_arn: str, session_id: str, cmd: str) -> str: + """Run a shell command on the agent VM.""" + output = "" + resp = client.invoke_agent_runtime_command( + agentRuntimeArn=harness_arn, + runtimeSessionId=session_id, + body={"command": cmd}, + ) + for event in resp["stream"]: + if "chunk" in event and "contentDelta" in event["chunk"]: + delta = event["chunk"]["contentDelta"] + if "stdout" in delta: + output += delta["stdout"] + if "stderr" in delta: + output += delta["stderr"] + return output + + +def _build_prompt(city: str) -> str: + return ( + f"Create an Excel spreadsheet with a 7-day weather forecast for {city}. " + f"The first row must be a title: '{city} - 7-Day Weather Forecast' merged across all columns. " + "Then include columns for: Day, Condition, High (°F), Low (°F), Wind (mph), Humidity (%), UV Index. " + "Add realistic weather data that varies day to day. " + "Include a summary row at the bottom with averages. " + "Apply formatting: bold title, bold headers, alternating row colors, conditional formatting " + "(red for high temps > 90°F, blue for low temps < 40°F). " + "Save it as /tmp/weather_forecast.xlsx" + ) + + +def _download_file(client, harness_arn: str, session_id: str) -> str: + """Download the generated xlsx file as base64.""" + b64_data = "" + resp = client.invoke_agent_runtime_command( + agentRuntimeArn=harness_arn, + runtimeSessionId=session_id, + body={"command": "base64 /tmp/weather_forecast.xlsx 2>/dev/null"}, + ) + for event in resp["stream"]: + if "chunk" in event and "contentDelta" in event["chunk"]: + delta = event["chunk"]["contentDelta"] + if "stdout" in delta: + b64_data += delta["stdout"] + return b64_data.strip().replace("\n", "") + + +def _try_git_skill(client, harness_arn: str, session_id: str, city: str) -> str | None: + """Try invoking with Git-based skill fetch (newer boto3 only).""" + try: + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + skills=[{"git": {"url": "https://github.com/anthropics/skills", "path": "skills/xlsx"}}], + messages=[{"role": "user", "content": [{"text": _build_prompt(city)}]}], + model={"bedrockModelConfig": {"modelId": MODEL_ID}}, + timeoutSeconds=300, + ) + agent_text = "" + for event in response["stream"]: + if "contentBlockDelta" in event: + delta = event["contentBlockDelta"].get("delta", {}) + if "text" in delta: + agent_text += delta["text"] + elif "internalServerException" in event: + print(f"[skills] Stream error: {event['internalServerException']}") + return None + print(f"[skills] Git skill completed. Agent response length: {len(agent_text)}") + return agent_text + except ParamValidationError: + print("[skills] Git-based skill not supported, falling back to path approach") + return None + except Exception as e: + print(f"[skills] Git-based skill exception: {type(e).__name__}: {e}") + return None + + +def _install_skill_path(client, control, harness_id: str, harness_arn: str, session_id: str) -> bool: + """Install xlsx skill via shell (requires Node.js container).""" + if _skill_installed.get(session_id): + return True + + # Attach Node.js container if needed + print("[skills] Checking container...") + harness_info = control.get_harness(harnessId=harness_id) + current_artifact = harness_info["harness"].get("environmentArtifact", {}) + has_container = bool(current_artifact.get("containerConfiguration", {}).get("containerUri")) + + if not has_container: + print(f"[skills] Attaching Node.js container: {NODE_CONTAINER}") + control.update_harness( + harnessId=harness_id, + environmentArtifact={ + "optionalValue": {"containerConfiguration": {"containerUri": NODE_CONTAINER}} + }, + ) + for _ in range(24): + status = control.get_harness(harnessId=harness_id)["harness"]["status"] + if status == "READY": + break + time.sleep(5) + + # Install skill + print("[skills] Installing xlsx skill via npx...") + _run_command( + client, harness_arn, session_id, + "apt-get update -qq && apt-get install git -y -qq > /dev/null 2>&1 && " + "npx skills add https://github.com/anthropics/skills --skill xlsx --yes 2>&1 | tail -3" + ) + + # Verify + verify = _run_command(client, harness_arn, session_id, "ls .agents/skills/xlsx/ 2>/dev/null && echo OK || echo MISSING") + if "OK" in verify: + _skill_installed[session_id] = True + print("[skills] Skill installed successfully") + return True + print("[skills] Skill installation failed") + return False + + +def generate_weather_report(harness_arn: str, harness_id: str, session_id: str, city: str = "the cities discussed") -> dict: + """Generate a weather forecast XLSX report. Tries Git skill first, falls back to path.""" + try: + return _generate_report_inner(harness_arn, harness_id, session_id, city) + except Exception as e: + print(f"[skills] Unhandled exception: {type(e).__name__}: {e}") + return {"success": False, "error": f"{type(e).__name__}: {e}"} + + +def _generate_report_inner(harness_arn: str, harness_id: str, session_id: str, city: str) -> dict: + client = get_agentcore_client(config=Config(read_timeout=360)) + control = get_agentcore_control_client() + + # Try Git-based skill first (simpler, no container needed) + print("[skills] Trying Git-based skill fetch...") + agent_text = _try_git_skill(client, harness_arn, session_id, city) + + if agent_text is not None: + # Git skill ran — check if file was generated + print(f"[skills] Git skill completed. Agent text length: {len(agent_text)}") + b64_clean = _download_file(client, harness_arn, session_id) + if b64_clean: + return { + "success": True, + "file_data": b64_clean, + "filename": "weather_forecast.xlsx", + "agent_response": agent_text[:500], + } + print("[skills] Git skill ran but no file generated, falling back to path approach") + + # Fallback: install skill via path + if not _install_skill_path(client, control, harness_id, harness_arn, session_id): + return {"success": False, "error": "Failed to install xlsx skill"} + + # Invoke with path-based skill + print("[skills] Invoking with path-based skill...") + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + skills=[{"path": ".agents/skills/xlsx"}], + messages=[{"role": "user", "content": [{"text": _build_prompt(city)}]}], + model={"bedrockModelConfig": {"modelId": MODEL_ID}}, + timeoutSeconds=300, + ) + agent_text = "" + for event in response["stream"]: + if "contentBlockDelta" in event: + delta = event["contentBlockDelta"].get("delta", {}) + if "text" in delta: + agent_text += delta["text"] + + # Download the file + b64_clean = _download_file(client, harness_arn, session_id) + + if b64_clean: + return { + "success": True, + "file_data": b64_clean, + "filename": "weather_forecast.xlsx", + "agent_response": agent_text[:500] if agent_text else "", + } + else: + return { + "success": False, + "error": "No file generated", + "agent_response": agent_text[:500] if agent_text else "", + } diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/cleanup.sh b/01-features/01-harness/02-use-cases/04-weather-agent/cleanup.sh new file mode 100755 index 00000000..1d18ce70 --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/cleanup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# Weather Agent — Cleanup +# Deletes all AWS resources and stops any running servers. +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}Weather Agent — Cleanup${NC}" +echo "============================================================" + +# Stop servers +echo "" +echo "Stopping servers..." +[ -f backend.pid ] && kill "$(cat backend.pid)" 2>/dev/null && rm -f backend.pid && echo " Stopped backend" +[ -f frontend.pid ] && kill "$(cat frontend.pid)" 2>/dev/null && rm -f frontend.pid && echo " Stopped frontend" +lsof -ti:8000 2>/dev/null | xargs kill -9 2>/dev/null || true +lsof -ti:5173 2>/dev/null | xargs kill -9 2>/dev/null || true + +# Delete AWS resources +if [ -f "resource_info.json" ]; then + echo "" + echo "Deleting AWS resources..." + + if [ -d "venv" ]; then + source venv/bin/activate + else + echo -e "${RED} No venv found. Create one first: python3 -m venv venv && source venv/bin/activate && pip install boto3${NC}" + exit 1 + fi + + python3 -c " +import sys +sys.path.insert(0, 'backend') +from resources import destroy_resources +destroy_resources() +" + deactivate 2>/dev/null || true +else + echo "" + echo " No resource_info.json found — nothing to delete in AWS" +fi + +# Clean local artifacts +echo "" +echo "Cleaning local files..." +rm -f backend.log frontend.log backend.pid frontend.pid +echo " Removed log and pid files" + +echo "" +echo -e "${GREEN}Cleanup complete.${NC}" +echo "" +echo " To run the app again:" +echo " ./start.sh" +echo "" +echo " To also remove the virtual environment and node_modules:" +echo " rm -rf venv frontend/node_modules" diff --git a/01-features/01-harness/02-use-cases/04-weather-agent/frontend/index.html b/01-features/01-harness/02-use-cases/04-weather-agent/frontend/index.html new file mode 100644 index 00000000..3c4c12ae --- /dev/null +++ b/01-features/01-harness/02-use-cases/04-weather-agent/frontend/index.html @@ -0,0 +1,12 @@ + + +
+ + +Setting up Gateway, Harness, and Guardrail...
+Ask about the weather anywhere in the world. Try: "What's the weather in Tokyo?"
+Weather data will appear here as the agent responds with specific metrics.
++ Search these trace IDs in CloudWatch > GenAI Observability > Bedrock AgentCore > Traces (may take a few minutes to appear) +
+Traces could not be loaded. Ensure CloudWatch Transaction Search is enabled in your account and region.
+Traces appear after you send messages. They may take a few seconds to index.
++ Generating weather forecast spreadsheet using the xlsx skill... This typically takes 1-2 minutes. +
+Report generated and downloaded
+{reportResult.filename}
+ > + ) : ( + <> +Report generation failed
+{reportResult.error}
+ > + )} ++ Generate a 7-day weather forecast as an Excel spreadsheet using the AgentCore xlsx skill. The report will use the last city you asked about. +
++ The skill is fetched from Git at invocation time — no container setup or pre-installation required. +
+Running batch evaluation... This typically takes 2-5 minutes.
+Optimization is disabled while evaluation is running. You can track progress in Bedrock AgentCore > Evaluations > Batch evaluation.
++ This may happen if Transaction Search was recently enabled and traces haven't fully indexed yet. Try again in a few minutes. +
+ )} ++ View full details in Bedrock AgentCore > Evaluations > Batch evaluation{evalBatchId && <> — {evalBatchId}>} +
++ Send some weather questions first, then click "Run Eval" to score the session. +
++ Scores your conversation using built-in evaluators: Helpfulness, Correctness, Coherence, Faithfulness, and more. +
++ Analyzing traces and generating an optimized system prompt... This typically takes 1-3 minutes. +
++ Evaluations is disabled while optimization is running. You can track progress in Bedrock AgentCore > Optimizations > Recommendations. +
++ {optimizeResult.recommended_prompt} +
++ {optimizeResult.explanation} +
++ View full details in Bedrock AgentCore > Optimizations > Recommendations — {optimizeResult.recommendation_name} +
+Optimization failed
+{optimizeResult.error}
+Traces may need more time to index. Try again after a few minutes, or send more weather questions first.
++ Analyze your agent's traces and generate an AI-improved system prompt optimized for goal success. +
++ Send some weather questions first, then click "Optimize" to generate a recommendation. View full details in Bedrock AgentCore > Optimizations > Recommendations. +
+RLNlxB}Bd*DPSah68VP6bQe!X zPyP|(TkJ>C!Fa~@U>iKliX(c-*+rbh!SFUN+`&yeWcsnT7H0Q-1;qUEuA9!MuGZVF zfT>rPsaBWETSOo9(r%LPTtjElC$@ReN-GdaIb+78y4c>@2tn1E~^ z;V|KjN!V*3Ya}Z9RN|ggIQlBKS!75G7i)lJ7!Lpsy_RHvlfvu!dgA2Q)!3D7uWGZA zmK<^joe|eji0WHC1x--l+;Bs^L gq#562Uaf4ZfJxGH|Fc*M}Cs*#3) ViuvGX~Ij4b4Y3j{=oH3 zaLaZr+>@izpS$Z%R{pLXV@e%t*`x%OHwaq@eb9KYP`JuHS!m{gwqzGab|+yTfAYwB z5ZDqwO7s{<;*%tCtWX9m^C% >78QP!s=<0kPVW@{_rN=WTi0}%=p9~{3m=*&)xtHU} z4_E(?jTL#~LyYK0+7Ar9e99l20?+f|&!AsKu`J<+ds+Spv>;Xm10C;OLk{!NJK#w% zV9CijLW7PdsUxX=B=At$4j_D#=u6W6f-fu1hWW8T n+aVDQMf0j5&QqcHBs=}y2K z=Ufa>2>#r&z)Ec%&HLRpHb&BzP8*)OANPB)Hm_zJ=Xa(nUZbJ58pAVU#Q}UCWRE}w z9$2LwxJP+k=q{A%0h=qS*DwC~-3g#=z( }0IXzIb_9oeV?L^>B*23mevrWE89 z`!R$+xQkgQ3UMXoYItf4rKk&P3;YXO*4EbM)+E+nt$#F1TH{#1EKHSdX!(>>Roa(K zX((xEm9l7U75mGBe?K!2#F Sph~0Wg)5u G^zs!YtmS-M7-qGkCN_exiMP ~17lc`pBsNU4* z1U)l?MZiR0jc0{t4^+nw{2`x1bl|g574h%l7x7OyHCdZn&mgtUC@SLo{o4Jl;XqU% z>PuW~TyR{G6k)95=X(P7IB;xz+%f^_*Nd;l8IKu}3QJi;oL)n7W=`6z6-Z06nt-xj zR*_|WuGv-*#|Z2;^|196^_}J(lVu0Cw uT zFaYWcx3;#At`n?#uQRRBuXDE VvYPGt*Z`Kbl(xNZ-89BwaejGv00Z10b6IPdpgG+%_S*g;s&=XXARwfm+n?kVr=+E8T-p+?E32@(`d!dV=iG$XHJ_e-*fr2N3*+Nc2o`VfS4&h0W$wNcKDv(;I z*mpj9Hg >J$f zCXN6mkzZ%OYJ5#Fp&6Kp5hNat%qG#o1MzJN`%Au_!)C)uis?}Ze&wv?x>isXnL8<} z8r`Lt2KWlClIi5E=ZJlim0!)A6Q*(;YopsJt16=^TmG%9lq-0D#t~xGI*+DQqI9Sv zoG2wD_pMy+Wh7xo>-J^d>zHu<1y;Y;yVvCxz_GGMdXGt$$vb&Fx0Y|$>bJ*1) e();a4qvI{!@K i4cUdHue{oo*Kfum=EPOhuoAo5Bu|WANNrkPOx9Gun78uB#;#CCg(Vy2%Yi z#Ve(rD%brE!QEvnv$RR6`fp38K&Gwjt*Gu(Z9b>t@M3^mSn=Ul&e+(v5Hl7dKL{Lp z$ >jwQ<`%iLjwq;;1sKWHNYJZaY4mN^4JX&`~m| z*51; 7A;}1-Q$)yDbhZ YV5_d+8Ax?6MI44feDPSb;QsIQxJqtOIGdCfogPMLEY)Qd><|URKWA z+Ip*e^s5QCIxR$)AJS32*wA=VbFqyhT`C=&fI`G1uofWiT> bCH#e&z;lJ-D~J5qFU~c7sLNLxAXT=m6nl;pY{D*Nlgj*Qn8-Yb8@kdOpG4 z%tF~W6)&xKPZ68RTaEr(k9`M&N&%1H*-h@&>X+iP{T{%$;c@rcefqeO9e5TDGCb^r zT>ZYQgaETV&OA!CXgULKjF+V6hNG350J=gEeqHQ|+^l5# v+ZP9XP%rLK-{J0$%b+CYp&~(0 zM*~rmZjX%Io)_x46as{{ajz+P>1YrIm0a2G8y{EXlt0YhU0+znp+j(?m*7&~^9xuL zS#f0)R48cs54)G|$BJg!vgQg3Q1tI(L?~Ej0w}om5%l{<2%7N!8%sgcLVft(=V73r zV6gtr_@8h6*OvI+{d4rcd#dUsy7w`Wm4uS|d;fmLKidbs_Z{tj_3vZoKY>OkfrL;{ zB2cmtqUxT|XSp9c6f|Cj0Ri(sG8KtWY@7utUU{EzqBv$kM^b*thb2;Ep{mfQ{*pk- zJR}#-#7YWHDJ$v)zCr+7d>(eEXZ6#ahj}h6=`TJ)03Xn%O|_sOXw9b5@8QyG>CJ3B z=_Npn68aO_zidH2b9&xXL;{kxj15UXCPMwY7Dz4vEr<;B? |g7V44E26exQ)ry1C((f4xqi;O-bd{QF0p8T>v`<_^~_h3vnUk2c=||KA!V=uacq zu(bKp*v*-Evj2LWyql!)f4yY?$0YrCtkwURr2mGZ{f|ldZ>ZA$n56%=@B9Bjl!Sp7 zbikx5GyfYDYS-?(|Lu6Q(ngyA?_b7pGGv7qJan*kcEHA;<<-o%Lw5 HkOj~2EGiavtFdnWZS3A{OZT|HR-=$BL2f~ zPxJwXCM*!LxGW{R>by_kxUuwC^T`_66zR%syf!>fWdwP(Pd;0{tz0NG;AfodPGrzx z&SZD!64nuVIa9N8e* =u$0wi|zn8TD|9u7xSs|D7y2|HzWS-}HT(qLoZKz9!pauX(G;HgnF{ z>Y^_&XnI6}d+Uta>$cuvGFh7R_>Aer0+&UHeznd5UG9AqkQQ0*kG*Sr-@D!%d!!dc zb!<>VZpNoof9dmbN8NtKh#X3TdH($OEAsy }BP?8?icC)3>jOmDJ5zK*vJpzH?IzX>{j zS)>_ur~Q;^=Deq6_Pn*g9=A@ZrbrF*0EPy0Ekq2DUF8@V6cnAkJ~QwZwaMC40GOf{ z6wdu7&Mt%S41A+b0$s*_$DQs4yg^PyC!Fmy>AVLu>a4)JPrC_Qovu@fA{4NS1km98 z-jMwvJ~vA3FL;>#7q3sw+hMH~O`?4X!bB;5H2%O=@t-%$n@+B|QM`cyj86}zvl)o_ zT@mH@F>z`0mAcy(E=M8GHX-nIB>AHT9%DmsglZGRDAMm)U_c7o;eS@q;Ey>0qG)vL zch2=UF{r0|+&I@?EDlR8pT>5UGZ=x*_FuPRlE-7tq$*=t=4^KMZ$hrWw7uO6^=2z08#P+PGl5`&gAj(>OP< zJ9#VM%|WWa|JDi7bm`&Ky;vbN`sYuVUH7`-zVW%hFL204)KparRI7~vNYbt1CtsR< zmbvR}hTT^?T_Rp@r-kj^+0n`Q27+sHjU3X!FJ0WXkbspf4Ozbxhx^rcf$alu>t6xv z( ?_o#Oh9K4(NTiR+kHML51ycXm~n^1s3Cx_XMN=0l8L zR>xR-&72Wf+$Y_fC2K!9_rme+8`-sYy|||@c>zu5!y9j^Dc4ZehTn3XqxV{Qs KWCbOUNo` KkPxF0>qU}lCfF{B)h4$-1qkyps zhpx2THTScaYr!?UOVBxZ^NzeAmylx$=g(5gYdUUn#Twx8{; j0elWs)5Iz7$I-~A-T<*-Q+fd;m$AK(<+Pf;tDh}v;} z!qFNrwu^l5ZI^y`)dZdwq6ji9&|o+`{qIv{A`qCHestjxt>+)$#U?w^5)3unsT|%( z$c;g=(u0)+*4e|^6W4D$wXsm2pTYtDbownduaIHVoBcEvvp0wzon6|_v)Oq1c TNbyJT{ y8hz+*EA`2 zt;a8l^l|*Rrv#z}2Ni^e@}4mZnq cVh5f4rp2*p4Q244@C!&>4& za7(NVEmac9$r6Fv2|jep+v}Up9=Dg@V~jITuftym*xkPafOJ27O(V@HP6nDPHO4%~ zm~FIuE6u5zHcUKUc_@}A0;Rn^9g>QcC&*<1!;90|QM>fEyxu|#@zL9qB}8-ioH}gv zoB1r=)~y^ZY2Pi5`HKNPN%X-hHVB$+Hk`)BCknB_(_N(+mUk$J7DhpTI2Y8ESg!%Z zoP<1rJu2J7r4f&u_a>++`P@0PduI{iVMH}m;G8wA7r&N2{E0uf-)4a|Q^Rz@Z!Hrw zska|}U6qo+%lO(W!I1wZOR0+Pb8)PG$<*q8w4kPxdSJ&VJGG7ncL6%=7am>VmiodU zjlTAPKypHMnB!x=;MV~@nJG=`*oJlLeh?-%MX4i2J4 ^tFP?1w&SETuNc`n7j}=h +y$P9ig)!_G}Tp zRdqb9m1CCcHQ7?3A3xNnX#_eDvUBfl1w^0^FBj;r>(oo-qAd)?64MOcdmDsUcK?#n z8@?BsEveQ|p-E_8o7mO0n9ia~5Qcb?F}%S)Eo}dtWi}{Q-T&r%QdaV$m5nyJ)@ZZ; zbeQ+^{ gGuA;ep%Csw9YsF{Wtjt?o#>J!X%0-%J@bnlcBNtX|$%8oc|1M6Pdth zHPX^OS>t|tC~ZHmPgf`u+agPz%ot0pdQ60QR__~!TCKN{nB__kFrWI;n$Tb6xEm;C z?Bh04A7tSG*YbA6T@$3_iazmkGYZ4c?Go= Dx*Mc8K>p4S8lV&eERe}ccWZRL z$_qb9QUY`3m;T28=3^UdJV?&-D#(6)&lhOs_u&lXoGCiX xkh9(^Cd?fvfdp-f z^T>GBo1LDnuhPs-z~bcvyZ$+Gkp!CvL>$br%I!_?>pb}_DxR2J`qS@woBhJ5-MQFl zjc5IhKht<;g&$4!Z||+P&})|6>Xej*9zKC8Oh7_Xe_w$t-ADDSG&@{*h%l4m)%4WO z8*WA!`FP8>r`5uQCHXG(d54wa8yq$#dx#Oe_DU$M_jq~J766o}L@?_W sD-E(4CeM0nyRO;TCn?{@te;$}Icard+Dwu-1 Ybq7zlxE zi$_e1z_hSD_?rkmLtS=oF%Hg-o^>gP9>3clTCK7y^$N}L*!b%vw#NIZC}m;NgqJMU zcK42p-Rtj|=NXRjZI``GL&vty5m;uoho_wtj*;E!(qQeERisRIzv%S5tjv=}hh*o} z^5G6yUBsj9SU;n2cjI&9HF%z%1@=wn@SBq1NBr0gfT~}9t@}; HmN%;wH z$`MtY*EL( xq|gnu$PpXs0nct! zVlH`lt+Ep(G(E%CA7KJ31zy?&xWVxuGDmLe6K4bz{oPzAoOBv(vDrj^hREhdIOj@P zrFr%$XYzhteqJNWblFHwpdj!|u}Xiqangm6?=e-GVWD|SAuLU^Z%tUmOyo(8CCoF) zM%(BM7L6D2;9tS0<07t>driR-@!7i45D`MK4WV0Sz~Hc2$+rZOUSW4;owf-S&O`g# z>jo>;3hl3>B}s?iG+dZxa`A|7secyx+)-&lfP`GC771>cu_X4(nz+pRpVNdbe~E=5 zvAI58vGxWi3-K@rl!7WOrVUkFWRhwa374wVe=A&|$@Porrmvy`9HM!o_&ckc7s_cG z9Xh`LFwhPbLU-u6ZtHmLQ<*;Eb2}Yl4^gEFe^ZvuKdPiunfcENOT1u^YJx^G(V)w) z>)#N EFK$gF)MYtRdb-0$#=Zb&?-q#ba4%t&hj$`taw;Y zxZSN(CQS`nn*Pv}7EuP}N1xW0^`qHdcjyF{PT=>hb1_cB-HqLCS9J fXmSuOIjU^KP#r>P~((Sd|-CpdcM#u{hD_`Fy0H`-sZkCc=K|btv1; z+$ms8MA1*VQ_fHdnGTpr`9r`9d3MI>6k~mTf_5Ik^xtyZ3Q?^og+EjGaS{PU&JtWz zJj`^35%Rd4omRt2onPK*8)1PT92S2H%r{4POf=-lzDKUaAdd9DbR#Q8y!#~?H<&Bf z3*G8P2iJ5O)>nIwqL<6SIA;36&HB^5*3Blc+D9dp+}ySZ4Lyvxz0VfZ8sBl9)Z|tr z3Y)3*0${z{7xE3Lpdk ;W2aciTKn!`d6Sp CkPlE5k}IUw3_-&H%8o?e19aK0+eRwzhc8VqdF47GAtJtS&*`~o*A_RdaC z691ZhJzzDC)gslL$ApZX$^_K(pKu_T98TENqy~2RB#=0J{7j6f^Dt>N?)WX?6z5KI zp>vF<4zxTip+a>EJWqIi$WCEWTLY%%imKajlg@1cSVi;BmY2 =ufdTz+kTdu1!Zj#v2$e{}j|A`?kqym>Hl0w7Oc0i*>n z++Ce2e~VH4A6ir3mZ0_guLr4H>zyoH=sX(V{7p8dBhbr179-_DlQB;X7&Fy|^_q%G z(LcU}-d^jc$Bt$l#zH|fNCjo>P3HI3t#4gE8%2>%CyO?S7r9-=k`>6`9>JiuKf_nh ztrC<^?YLj^l@izpzvLS=xt6Jydfx3%3s&bpwO?2=W^;vwK4Q-xVE!QZPsod@JWQ=b z9thy2D_!6*7=f=SlCbrB&30<9#W-p(x-x9Cv9&Ey>(H_ND9b4hu7h!e(ooOx`s- z_VSqZ!06cVpbBT+((Ybe|3kK*U7c^6Z&~|u#w0)<#+z^uSFE_7KolKjuOV{AW5Ql4 z7NH;z;+LSFiKJ0vzTs{D^di{4AGgioeagBUEvdqscMfX#Cx$on+R+g)9YWgxux59$ zTeqC8)e0VLU~6LT5rdjopD>-=_r^m6e-& zMPE239AOkl6A*Mgu0j{OA+Pl)eofu4yDnGt`}rwIM-DSWsAuCWTm%ug?H6mRX2lnk z&Dn#;q+KlUnYpo6CS7Q+0tZE#%_+z9aMp4o`?P=@$uZ8Sv47H&SVHihc0Pr}d2x~d zn@O7yeZnyCJ*rnC#hK=}K>uYNB=(E!r@wUYEhoLh=@j`e^x5uvk}Y`Mu>nVf$!DD% zLOTL!I>R P=zm`L+evd-1w?&w~ujmEcg zU8(j2CxboNnj{7h(%<(Tr;llC<1RpE9=kXd1nzDs?&L%}rCL;?{-Ff_Nt^Ky*t^5S z5i2PjQU3;)=@Xwg;R3%Bk`wuKD&)G6_O|YGU{l7^#Pem*rx`pZU3F@IR^A=MCd-Gw z3f;yrx#=v Nvk|q~-;~~h1uJ?C>ZOVD5L#!%NNSHQDnf4C znpO3^*E%UrbT1pvsM@KcgpcL{gxukF*PEfB!2o|_d{u4F^Ti~#z5PTFGXa?AmkTR; zo8>A_>5tJOfWGYyTJ0+5cH!Rlr*dy4c}+28dGSQo(J1prmsM~dk2dZhPZDr Tw0 n`yS{L>DK16%-mXLPyADm1R5L)+DwHg3E`h^XC(YbLIxQsqA81aP|7(Tub3la zyWy-)7tz|YIHQi*#BMbD{5w#KINo@&lrBa9$8qYlI+3^%>E9Y~X{gg`n`FE3>NUVC zxB|iCJ 9G2Ehvu)_hvW-t*lg0K8|cWB zRf&dL>CcPeWKBIiQa)V(4Gc9MsPpVxO%R(gUTHWFkhX;L1Vop}vx-d4N&NK5#)4SS zb6lo53;x8>XJr=b;4j;^-;%TzXwycw2zxUO)Tfp&r|nQxbHnIHN?@+ql}P=`D;Mt% zF-|Q0Ef9F#iQdG1Xd$dAo&sl#V>=qWTIan_W4_6f6F71oT`o5Q$IHT@h$l=W@O_Nn z!iJ+?`jNoRJ}EdXdhFQSk7o%G@Jb$8)b@SKRa7Eid1^e(fW(vbi;$Rv0OG)yW(7Uo zLDxtM)mwy{mIyp~PZN`oXWz*zrD3vQ`-d@wIcRbUk-hdG%Z4l%Y}@N(W0D+Q#W4q? zx%BQ+ezSwROU 5gD(u zuYFpW6e8QTe2BZhD@_wz+m0o`-ssS!!OWyRK!grsjVOL#>9zHa)Jb3+Z =?oukn?8PLiYD0gOO9S&Wr;|!C3EARo>=2^~Xxt+*nMr%x*Wa>t& z9ZmqY+=eoRU9uRB>Usc@WU6qN`gr?EYMiX3C)%vx`J|LAulLIUOyHN~zfHzgO!=3u zp2AGP!;JZC4*Q0*Mn}8gw+;vE8SScB%exxc-Ay*L`AL_mCVsb5Znhsj3xli@VViGY z4>pS-?4a9V&a)EhNj~bx3dVTHQiKpwZgaXv1l606@7Ufe-orpAjgN`wbL785rk zf1BB=dehNljjoORIQaz|kRBn 3oey>uAFU%4e6FYjl01&F_~?4Ax8O8FX4yO9(gg7?9tDTAa4FUh oqakpKB10(YgI1vBj(do>IbD zfOI$N fb=n4PHkGJ+I)x7rn-G+f^1; zKk5oO^pIW4_k4Pys(a339kl5sc*a`$fN(DLUY$!0_plN%>$Ja h|Zf5i&argG@z-hHEhBW582Q!QJ$CtXk*^sGo zCq2;YrTQ{u*UXPu@*cvHaFfxVfZmIprakl0oscZclxKT_%FV`K;)YzQHp7a!8}AJ5 za>|4*^+v0CO%iJ?YZN;vPpgy;uDcMzxo_!ol!b=qmo-@(y1Yu+TpB_-0y;~TdNiCi zi^=>KEoxOvI&&9e*^tuQkw2*nKI2CjesvV R1X-_olL zihaYPH=EoKu9Spji=%NSD2Sk=4c#*{tJO-X)GIY7&5;%E_1oQ$HKb%7&zHAMuXiL3 z^Y&oxFoOFtzt0v(J{P p}NgdDKi2Z(Up5rav+2SjClfd@ECtM2U5Mm zcYNK_-XJ75@6Oz!9|x7qTTw05?d}&T*$#hf9-PA=SNH!E3l2A%r?J?qeq3ECfb-5+ znz<0Yh+dTTyPw4$!tV)9pKg?jB%*Bg+D@z3F-cyZ&gzUr03>F(!{6S+VbY(P720$y z)$gU;bRYp&ic8}!lsan-`f+sOSF+W5i}bqvu}nr@=e?P_O(u$$BkrxlNxrtAMlY8` zYtxtpaP8Pd_J2g|$%4MaTuT6&eZGy;-c>!a%8XbgSd~1MZZ=3B@z1Oula_h@s5PK^ zyz(+CcbEEpPW5aaETrsy$v+d>^5EAj%{*iI-Mz)mw#`tIWIkDfgo_mRbX9#FX Jw07A?(k^Z(E<0_)CM435OS@G?XZ;KY#tj4QQi^pmdg~_KX|9zf zuWZHSmz@qN0r=UFfIG|ffH&;ECgx7@J_a7z4&eg@7UWQ8gFM8_7kSFDU5ID}Hum|v z)JurxGhCdh1FG0n1fToQGkQmKw#ZGX4h|W#r_hlyCUlHYZmn5!4si~h$kp#vjxft^ zol?c9bxib4IUJDlNDU0S#^Z$zEWa0avL7c5^bq&LzhW4$XsnJ+Vqe l6s4ej #hVKe$3-{jn6>p#b-uVsm+6xFc zJF0mJcXLFve6v)dY-#q|IA+5+6VHKj8K+7kM;em(fKk`QQ-hvNF@cj?q#=lekPIXf zQ1NNg!b^aY$o99uc>DPNI!bkEAZM0Z5aG7hu$CSih5a&f_n#Jyju# m@YkH{aYs0HsOJ8(w_6_&*b;pa z)fpIpZuhugE#Jn5#nj;N0Wrme9wVNs0=uZVtwRY@v8G>pXxhB`v~v>lwA%NV-D(i| z9C)1K2p&34=*1{5HPJnih+3VqD?(lb%|m}c&etQaBY(uT;cJ%L3({2<3ic!&OeV3E z|J(#Y6h5TSw|5SRs`;?gC *nc;HZjbEs_nR)#A2Ib;3oJwDGx+`zJ7qV z1u(UC8Aw)2mzVYs;VR@D2rs{bk6)e^Risk^^udPvyk=xZ5%x-!FBzPU*ocv*Wb0rR zz+ol*ZzesuqBpOL(LCcm3DByiA?2Ky#i#03e}kC^ACRvNkw{_V{&kc1MJdiKQLdv< zOSNDFN-x>m0K4TRR+CL<=)(X(9-w})j5dw7UvuwsCDXvQU5a-=H`cJ6U9Q#iEE09M zdPIqsBr~#4NsSk)0f&WD=&gaSezQ;P!c<8~;HFT*kPb9R{q!LPC0^eVCa7?c`vlQ? zlI;N$aOQo7JS!afl&iecT3vP=prsOF!4n9NpCj76dY#H`oes2!y#IW?u*ieQ(}g=X z>p!#9KAoZ``~tl9T;l~jfx6TG3bYA+?eU?0rBwRfNsP0TeGVMJnXzbaKbP=Chn-G2 z69UJPJ3!>%%x)RZxpk%8-`tO0dU_|oc0Fz)dTaqrjCtJSz_VFHCH#`g%MVh0H*8cq zyImjJ!0cySjo@Bwr9n3gy;j#GK3w^0t}quwscB&KeJZD()_YQu^hV&O9Vj?o;v;eA z)s7Baz7fXKCY|PQ-nfk75j=*y+a2m=(86QDb!U06fYPH9Z(kESbkQxR(-Ws5z!<#B zG hx)ThZoc~ -b7y_gspWUWH_F&h+2O~_^v~Um{({`2 z6X*m2+V<*)kz}{XNArafJ1w)YcY59H6mjaTmjo1N0q-@Qf2u>-wg>z|uQd~ZfV*L@ zF9(y^TEC|S)@0V=bepZ|#?!)thuw)w{xdgwI9wz({bXddH5l$MU2boZU-wu68oTNk z?!32_Z$@b#$?O#KlvWQB=x J+ zOpuIAx)g83)2*n?>pmV}{2e7>9^_Q@Ac@mO56OSw#SsKYjWoz=w)s+F)sn _c#~e@86SOAByYOQ@^KELk+>_3%2g(Z zGhAQAi+1ERLSaLC>~DwerE)YMZy2Z?=D<=C{=NbpVQv37k7q-J9X_zXYqUpokFQ%Y z`v6JjR*MCz7Eh2F(c7(?&7O}zLg_J;lfzwJ-|r4#MJ{V~FPj$4$ehX#@I-7)*1UYT z74`DiKE5asPn30SI|S`4IiwVO MxCC$)p9?ZD`b0GM|7Yi;chJDRpUJ+ampSnto_FAo_6ckW#FO+4|&4y@Z;; z+r~>Bz8wMrCL`xCw5Y6c%FB9GDV_Q;!btbAU?9P|)L*>=_rK~ mdJfg~m%(H=4LQ4CM7sb-Ufto~lzxc~=o zuI&LoaB`4zm9ud=ZZ!&0@5Va@%%)fEerg&bfD_tt4XkTcD$JqOeXAS1r3`i1XV6Nw zCg3D`NQ$?-dV1zG-+V()8XUJW{5&Awc~B|3;5;2-hf2sgbc RQNPt_LCJbATMxNt!@yVU;RKjyL*m}n6ZJx`V>`4ZU%cMz>j{&%_rP08za zA0+YgnRpjyz^`DqE~mHEa!6qF0*^qpz*z&@B*Ec=i81N`tUss2KX9ttsVreMl7!#D zTX0BJN34(x*A+&e{U5w+G=jF*je|niHcbj1B0soc{M*elp= zoqa@8HnP>GK^G#Ln|Y+H(6bQBxs;oJypt~DN$0yK$m&Am>OI0VDubAmM0?yC>1x0H zNcXviUU~uENUV`BL&GogK6U;*y1ZbIadA9VVyGLmgz~(wrnGKO;|6Z-ct4zNR!*PI zmHKID<+SL%Twiu^j-94cwy}j4I&y_f<~{ss+LD_PfDEcxti@G+pJVPf#=fT_YQAD@ zxuk?7h0N`=ojdJj{!S0vB*rAOTnJmpD>pm4d|LsxTyj(e3T?g+*t`MbaO3 I?jd&RHUnoa zt{Jq;`o V**1mbUdSlv znxupGKV(er#y^-(cHv}6LQAbM&v*3o2RwiXL;dro@6j$*+P^yRuX`1SeMgh2%o?OF zLBMiK=_5-F@xElbeDwZaxc^8NQieV0@bM8lWZ#`q{&ErOUO?EsBObZoKbsITy4reZ zb}KP&zqjf#HTolbD!j%u0V#RQ+s!gVW DZlV=6%s1)c#ZW|fkkQv$+gO^kU)&J&bMtD|dg?-`L9q5aAkM-q!Zxn%1q!PrhQBr*0u z+f{A$3uD&fIF6!-I(Y2%Lw~_Q8)!k0<*Z)i=Zl66782J%f*}F~z3Kccs@FgozPn|J zrLDiKQuH(M>3T-m-9sHp;#LA%T02pB73WKS1=?Z~VucJYP{ehq$pu|$-~ex `q{(aN9qP(p2#>Kay5O?k;Bp!J zqSJYU<>B_{V0%Qpde*?4Fjlk9@?xUqywU{qlac>#Kt6HZ0iP(`BK~!gLu5gWH?G|S zpPTCwkCiJr(Xy0r(_xJ)F};xbFp|xKVVfD08=KU|BWNoiR(UB-GxoVq4-h1UeaiPS z!(D0K39ZT`UNBxSVLDexQo2LOR7;k@&AGPFRX&%;kn3m!zgE|~G5vZIIme^po~WrY z_|h_|%K%zXqS(Ra4FxP{ Q`-PwHdkMAXVJgqa`gd5g zWxxeQIbezj&mM?8ODvUo^oJ7EbN$x_hdzE8llZ$maXcT2n~p#@rl;=l5e@6YFvhQT z72sFCZ-kXDc7o>)<5=%hPk9E|2({gq4|#~TKTkKk+rO)0+Dt$Atn(E8Sd~iHDpMJD zVp@V8J8&zpH=0veM$n_T;g7NWyL5~){q|->?HKV(;0N}v)f3M{T?)6~IPyON96P)u zFPni5VU#^gviDze6ybT3K=XTT8_2TzzTO{?wgW#RaBC+fa-^2frPTu8oIu+Tllstp z9U#snNHY_N2k)))hh@atmwTG?);7Oz?F38*1G|?Z+L$Ws 2YB>%!@gv#P`2Rmt@GeO!421 z+BFrP-WSS#K@4-Uj>|!KrAOFF2*6L%dh7GeO>-u}#*#>r{RX{`I?vmC9U5#7io1I+ zl8(f%be~OAfHvmUm#R=napxyFOplbyR2#Fhi)P`_K0CQ;|A ZSZzRUcs zBjYzGBT4p}AF+Wp@x38=NgkX2{EXG@j Ll@ry_L)^)|3AG;8MG@#Mu~K!hTMlUQ_@gL2*y&0 zqKPLwxBDTf2|@p97+#c_a)U#wt)0c;VCT#-Lt6nLo2$!WNV|@~l7|O-*zEpx)`(KB zDTD58^W1W-_}m|v2>%ep*U`Lz{`=ZC 0aE?FZ?=a*fx>1Q3RZiMq9RfBPjnW^xjmQ!3%&mg!6Y zaho8yPMvuihV{z5==|Q8xHa6J=nn&;c_eRitG%mj7L+^1em )Te)S^%n*lbxuTCeaIgoQU2x zY3IPs0)YVas--zK5r+g!JL_=(!-3@HR03!|(CP4VuON}CK4!;;R$=>PdIdodDup{w zZ_kCQMON^M`Ue-v`Rvf5fabFLQJfQ37?*KG3VezJauk(#ys0Q5)yPgQM<_2y*`|4R z3WN%@-f|cU{(grRsrmNI!0`i~$R7iYgS!XAn+o{9{{^7@{gkP~s4}U?!dk*{`cBVI z%}sf%+O2|9%M&q3$)Zy0J47ahOE^?0j&p|>w(xs+cSFrP!>e7ui14Q3ObdZx*^$`I zH8k0>NrTeazc}f@ #(T$u76ZYLMagtkVa8JQ0WFqrKG#NL1L&O6e$Vm?(Xgq zN$IX3M4Evar3M&a&gOoe_xhdpy&mp!e)swF{QlwMVurQXckQ)Tug_L0d9V4o{UySa zs_cyhj$OGn_RFd39UfQ Waf#kB|7fG1^^c<_ps*$%{MBPZp~DSdF>|C@{Q&F}GzKgF {Nc0#$xqR`bI-5jQeQf;tMe|@mDAC0-lOsv2Vyz zAGb_ WHA!9aVM^@N5@)w2+7{-Q2E8GP_4bdJ9KUz5r$NhiZ$bjsL2DM0v} zXpFITki*YLby-6*xYb|rsLB;mk2A^)YX{lAR6{%*fts32#-Rq&h);2@co)T=&W@kS z*FM*q8+-DsNs{dW4XbW%>X6kY7Hpy@H4c}{x;6*zv}wgDIWc%beeaRQj|bEWLyns) z>aqKZbo!tx0J5tTV@f$9+kF4jZ|M>!4>)9Gom(Ne6RsOsj|ukHe|ii|b=Y!@WizJu zi{y*=jyFT =${gR%^{{z`(K)M2!(Eqf |WB@RgRD!>>?RC(Vz`p+Sbt(jJx%=0JAzmD@r#@C;+sRqEBDYL)T zZ>v8N^*T aB{$9|L50N8Ao6pZ39?KTTr(UaM*uD!Dr-;iCa&sKt ;yxMemh^Sxu8;w zO|C};fmA#zsG>^t!o8a%p5+HU^@q{=x(DMZ*PX9w6%X4#q25DhCkyNa5#w8%J$O4V zTf@(f$k^`dMq#-gBX%UHM@DDqnO&aXr|xVOE0>o%e$R}l^&azrqtPxJLdEUM`_|*y zE~2U0B;P#)oO9MF6O~|@N&UDQ1uS2^TWMR1{{`|s9f+uR#2JE=B1k@XulE^SYgxVT z%&=SS5N23bz 4j;bi?(|~&{Ua8{kbUQf({M|&r z>xTg>95d0t6n`i*Epi{JPUaTEJ}f_?sQ-sv )$ zronZW{(jDj?|RX6J@T5x?^mVs`@JN0f~zo tdf73pG)Kjr4ohB^-vu2X(YLmWrcn*nlTNfw1VvXGtqsX jX2C33-IZ>Mr%fsS(8GbKp zqZ0b3ETK1y32{J!IzTW!ARVmqtPePE 8SjP2dROnQCf{g>8f z!S*syj~SdUT*}p&-+kyFX-vt>avv5JHG6aQ(Z?tZ=hAA`$mk%d#ZH3{rJ`!7)3sqd z j=dCb*h%7 zUZdgt!f#03243ZnWs%_W+GS %T$-(lZcH1HA8*diJ(tYvP^mVN60ZYyz8B12&oA`9 zF13ejG~CTrG#mF^aziXnu2+u}3c9y@N94;TCMY|$S!+HKvY*&T)2@HlyKf_oei2ou z5KFV%`o!lk2wPkcU2C-v -z?WXh`4sxNrRp*@I`voE8Fy1t}+b%2d@ zY;cUNb`1Y*iAYc#3?{78&+u5>0AU?#$Q~R#`%J;>CET 1;&X)n1fCn`x}^=<_+y&f v2beiNG6i zVd~;L?qyiVQk6~bSkzm^Kj_ExlPsTq4aD7{zuO57NO(H5w5!VX!RFcfG(RfTYfSL7 zqpEZPn{SV(h3N+LEU7icwjAJ<+m}Fim6WyKbF}ocIf#hVoIyv@0ms{+j2th6=X@Eo z@g)NKUGW}6t02sN4OeX`o``>R{Ml8AUD(C%aYAhKvm^TGCz$+m);;ErFuxErJZ5!) zR9U%+3vK@r2*>2d5}RLs^}~2*Srl{qWv4&Q0m yrZO&V?uv!X{i}GcS zTX;Ls7BcUgWjdW8&sm-7A%(qMBM>>^Vo4ZQ#q;`;v`r%bHu4nWD(HrFJdsl(QKTUb z*d-;gOe0#ld=aY1O}wc~Mj|8OLGi1;Y ?Nyu3#G zwV(W%O+H;}v|nK&>1$g%7)y6XTEmbZm&rWWn;j63{tOoaHR%_u6(phFkqD7_4PfYC zNa1y^`r5gy92!{YuizL$NKtV0H8bZ}VrhbU={4=pE4DO`y*< He1{zYJ_Fkm519NX11_Gw}EptHiv zJe6L&8m|!@etpJ9U8GJfXjP{(L!WC3!K@fF#7>C67Kq8JM(}_^^kgIUaK#($1sqvG ztwgPS>jkrb>g8T;ywXkCcZ65QmMPrM8O{TYSV0=> 8}kj& z5d~P`Lbpega)=cYy%t7=j@X{-XV>U?gSCIeaF-@JST@)zM@LHgxWCA-UpiP%CL|dK zh9_axD}75*wA>|^tcq%$ddv(+lNJ@~JMQ3G3A#N0_E(ou=fHY4CtO-n;tiKO&ui|U zUzIj>r?lKgDh!+SfDjW6)#WKyz@5z #c_g3 7m@3UG-6K8UD1hFjEARkqf9q~Ue$Pp^ zzZH*4v$pg7>*kxPtlaUq=Mm{wq)t6=tGF;fsBiiqD6}ZUUi~YE%XvoS_d{TO{iTT{ zh}mNlHGBGpYsNw=xON&?*05h~IrUu(iPo&tddgYoOP5XqwPjH0);1X_+04kg)+NR6 z%m?D-`5_LykEZX}3q}|o>p4aE?Q*v$TKS@ZmoFkV7AH6FQ?lkX{1RF3$@{K{uX>Op z=OI!KkxP3IR0T4;M&AIt)9t#nzqE>XG3Yw&3uB;=nWjG$&w=p{>O(;bV_I>If;Xha z+Pry&DZs?2-9p_fwp7;0pxp7#%^a^s(X4p)W$(FvN;sg6Pivw1c+Tq@$N-gZvRl*B z=CceMc^aZfU`c{Z^I}a)E;sanMXZ<%eT1;{Yqx7dvwk&fvLk? 0o-mOOW0p7`#t%W03Y{#WOWX%U9|kt$9}cs1{49KQ~7+oY<)+KX}{xt`1z z-y_@bH`CY= 9jB?`>$h>(>f~lV|vT506>Th)s9QgWpNq zJ-Xw9ykGqo1Ja|bNo$MNPCYRgxoA-dnSJgah=bhs9(DFWjX#4ljMTePR0!)(5gkwq z7-;ZmygNwo?p#KO*q1MWSTtQTR^ac72<3K)+<#C=FI7cP*=OR}#gbGs?k;unJ#S%5 zZH7OP+ZyA!a`n~|93)-Y{o~+x;6S~5-&{CltBOB0)Y}ea2(L-(c~U(YDbp@xWZ8Ki2~ ~gF7Bj>F4QjMfv zs1S-T19(W;=Z1AS$3!Qj6F9Ta!F2W8$xFv&wWB;Hd6S#0!Tk4_$fJca>|+Fjt)N5) z__Phiv?W$WzUxn=_fJBl3G}mwCG~5SCFm =RyiI5N&qFEwgPcRUUoK7cVV z`uxj&S!Txe;z&BLI80Ai&8-#mGgT Y zV+wDM0`2uKkp_heV;U0c*a2ex*nrEVLU IH9c^cwA-WMEMFY*fPdCC%rD5O zeW~(*vX45zfY1oqJ$;Ji I=MUzhF-+b}D)KAMf`G%}9uH%! pW5kOHgVECZL8~x45`{&ai+Pz3=)E I*~ik%-g*+?6{8qh -1spXJyzrQ-VRRBR(w9q*VV@9L0T5aSQyC3T$8VZN254ZfPFByK zCTZ2^US@vE)S^F&D9xM }?J;yW~9 zhrYJ+d#*caPGd0~9rH|@2A*6wf`>CD%KE@WaIp4RO)26y=ZRdK`LNxEPp+Pu+kom& z#hJ1&aj6Eb=l#Vx9{a^X3H!x)DbC(|nxVheAVKCK#>SHyKUDH$CqmMQbjH$NNz7In za@>p&V_*{uii6RHbCt&n$I?inMH%nyaLg9H;Ixp+aQR|EI8dUVO{>Ac{;V(1ls?O^ z%~Gj(eYDj?C^xQ_dpw%B0g8iLMOR3Whm;W$OR*)_a|&S7s+(7MeFTJG?SpItl4xK| zucv II-nGQ8ibeixJL0ZXbl{~6SNxI4ABYH70gGFkp#HtOnIq(rmw@Im%+NY-^! zQgujKRc0K}7njj|d=N%SLMpWY?V85B_p0* lU9qIF(IC7iXK}Y_;qdi@ckb{5>mA9v!&y%iyOEWD-l+fIe`RVAjdAH{jFPf zz|_o+7g4qkG|9K?n;ESA@_qt)x{R7+gt&qBnAR@hxYw=Hxhbo86C=h%#%w8qoF-8X z22NPV17?|@H!wpqONuC>t=5YCYwp&Xkok1dOg9EgH=`hVttgukqpEN`Y-3-v_Mz{j zTgk(3Q8ad_5NWAV(*$XJ@CuGe)z<+RQ|h==8>8yE;LfkEdB5LIuvVN(r&ba7M`IAD zRC1_xnFS^JjluNSsGb6=CqU(0DNJv7Hor)e1~v=X_~>-8o_Dwp!( TtAk}`b|iNI#$4D!%M5~aJ%OI1K~#XYzrPzI~dX-YIyFb2}ryx{9s4M4<>(S zmNQP4w1*=Mm{i8lf=S0|mDi{lIFK)Z{lGTxowQHosj*+gqE4@gN!@h{Yc;tR`MIPg z?vGlg*OzsLGB*3-=Ik5jG! -%n+X6`r!;osK_Lw@DJJ=zjdbjtgmp4BgTqhJz#WQ3qSv^>)?6$=Av~C(VkC zTDzwdO9#39Qp_qh0he%Eqaea(@})nC#Imj<@6jh$$;8gcv8ucj{>1M#{VFLarn z$i;J>{Ju<{GCPmKvJk%ht3BR-s9B5-W+^qak*9l=}x$08hm#&(ry zlSPv^^(^hYE-s#c<^k%1D3WN>p+dbnh(d0FF=g^YwcNM{#r?xx(^&=0yDo|xJFrR(A zeIz^9@4jbL yQROtXv6_z{? z-?@-dchjkS%(|o{ecf`mnj~kidj;$rXEfmA!_>k2cB9Q==EJ$!A0H5jcwQX dAWlP?TQ01G{>pUF_;?5kL zehXjRUwMciBaTB&;;d=7Cd+z$LsZcy1?4Q8a($xz^ah;>?k^A<;* dDl*;6j74gPFSBp*F#gzK}ov;%k&q{D& z#>WBos^wK5`$6h7D!hj*b|SyF6&@U$v>Uo6X3SZ9Zs4mh6f*?obX4Sr*x8=E`t;-B zdM6N+zAa~>&oXTC*N1TE%nUUJc_0B^)f{fta z1L|BmaW<^z*Zb*nK|k1E7`~jw4iO@VelFR>L9kEdJ_?lF`Y$&Lp!Qeco_^ hi58#%8?Fw>I!6JglMldJ#l%GRu=qWn-=2!wpy!O;n2-~3;lpu!L zQDh1z-FNCc_v%{-u1RjEpptPPj3<_=jf5bosWW6kGNUd_KK@3F{^+zJ?%AP1ZFgE6 z`{pYjg{MkQodd^uJ9+4|OOrHT<+o6*Q~69lEbo~ZvZ5LZ{avcv-h4p!+F;F( ZoR9D|Tos|cy1FZFC0E{EV>V)=fhA1nPDKS?U^k_F@x6@nWU*~RvdbOJrmFck z%9iqVQ75xb@987_Jn&q`huMYgq~&wyv?PQcs)pA8=vd14-GdN_OYr)goG-bfG8x-Q z#A`;y`I8-NHvk|jR55Cs`pSY&w L#RY9X+ zmCT$rtqIDj7%w1Ng%$oZ@J|xc+1OW3wjK_zUI!htLWnV|R=|mf#C{oXVe^cWERp18 zkWGOD|8h>QyaFZ4{pVKbG4^saYfe4 ?L!VQ3)TmU3BTC#Ti}(dcaxKV3TFkS(Wj zz|q;vck^g!XG@~JHZ>umZh^*bvNH6**83u(1JJ&Ewbr7n)397&M=FuK?B4rN4&qhJ6` a zJG9Uh{Q`wl8>N8%hZVQ`VszdU_n4G6FJp4G*!cxDWre)|J1SkUE5F=dHO1?e*PEMt zUic&YZ-n?9`_#@aN;#gupJ3&goakhHC8YrRDW;^_`Eg^U=qq;JPxY)44fkESUqXWtZ?wK%A zWnX;PBYztq!0R1dw~R~y41M!__TDSPSKHgrxq+@L Kh37FZKTwrecsPd2;pgNEJ0CS;#5K))PLcMGZa+x8h+B!# z)VJC=-Ps`^i;Cf`Ev)tnmr!oVu|sC@NvS68^|gv`qM^PX-F$txeiy%Fij#@I3CT zuaQF5jh_MF59)tx^Ywa5Rs_lr_jaO#kcq-Fio 9G+5&oICBNcPh>D0JBJ0fhZGa`;C^DYG`7GcnbxZ qr z+5^HL|l)X=Y2G8)tf!a8lz1a@I##^%bS%eKuqb4lWwD(ZgFJZo|R#SMBk;=A-Z z6)!1F@8iR)O1AyV8)-J-Um|QriWs$P&11hyRlXc$ MsD8(-)l(@u6`jii5v!WNCX&; zYoNcD3Yj(1wN =tIPQPlfzrN~}k>@`YlrZsr#KoAd&Sv*Q$JMv26F}N&F6o)D^y!l!!BUJ3O>q@u)Vzi4#xK(H(>-!4XluA~@A;IajQgP3!F$-Fa2M$7 z@HZ1DR^Jae2SzDoPJL1ZR4z-$7DxebAvM*V8rG`C`Lbf#0ZZ0|1u0H9_!weQ>CEF0 zS+MFaVwHY@FI>&>x)t3E)H+@F`T0rb!4GY5z66v?f7B)b0#$%-`$T3`uW#)^u7C5^ zDTN8YG=p1x`zbK4Mq +bV1V$j1B= f%R{bc4 ~Ag@b!!eT{O1B@4nuGAJTIdAI| z2VWXZO70#>R$;pb6Hw=$e zB-+0`qjzBS*l`)jY9X?^sl5u(}$zJ~vggQ5-SZ!E&{HG>V#Br|myHV;^ zCQLnGj*5k^=d{YBp-V<9q`VBA6}`-38`wCVLPbgoc}kAIM5@8W%r3#zJe%H9yt8{; zRQQSTy0VRLJR~ssdGw+GLHRi5(d=y5rKE*?vKbi(v7!;SlscWO_Z0Si1xDfZhm@G6 zr}+bM*Pk{zc1)~S*tudF4KbbmJUQP)EwmDz6_wOQtrNaD9@n+XM&vwea8p-f%VMCT z3!!6qHoxo!(g!WO=^M`Bh4}mX)6wDJeEf)2{n02Ju_ED!`=kybWZBf6P8RbPhckBd z+-G>npaYa7_zHEjbei6Zbafg(;SMY?wmX@y3B1l0)ZcoOmg>M;ey6=vrI8)V1>*aW zoG>w0L3k3)GE;)BuYek%e*3nXUI=xkz2axZU0PQR3zMqfM*QtOTg9Y`eB*xlU`IZ* zS|hXzBa{V~>#vmz_oVTcrLE?ts}Zd>)!wbJwX1@fqpxU=ch!n#$F_qN%&*?~VvQ_8 z_p1Ej 5dbBAc^X|{9 z?ncUwdlyc$nNWCHZe^giW#({s#oFY*}5!UMZ1VLxNAnlCRPbO!~QY3CwtoF`7Sip zMcYC^OCkB?(|L!fG cSG#)~M|WqrFXB93g} F13-tEclTCPhGjH4|{DX*x^UR2z|rBQSN(K!`-J}Oy# zON%^OZKo!>YBVmrpK9>*BAII6AY%dfUI+esTdTo- WVfyYx34Am7LTLTU z?!%Z1A<9nRuGKaVsvy%ly%ljBG;_5rxW4zh%Nu^?+B7t5SwNDsWHYMJDc1~d9Thoo zI~d%2?R$)by>;iS-KMVUx-Z`ZyzE%`;Q` MIC!79pY~1CDG&-Z>H09C3YsJ9|viZVI({`x1W79tOX^yDX!Fs+hb` z+`BZ$9m;D&2V1~h)a7O30Yy4hvD)rfZjEVM7x+maXj?e+oJ8a}xsoy3!|+SbDh9^S z$yZ(HE4kF^D#Mo9zIQQg1^oOyQ)J&A8pHSZ@y2bp*LVsziyKI{itR7B3hHz$ItXnF zmBAmAu0`TBrbewUdc+QV;x$}g*w>v?f>2CvM`)+=HZq^RP)WiIQLec^m6tHs|EK z?LHgPH rrfI7B4$0cRGU@K*;1i*PIgl*!|J3M|&`j?zi zKJWv5yOYf2oKvo5m?McxA_8XuqJLbi*#(cPJ69)jh8$PeZBVvf;2AtDL*Mg!A0gv5 z&y7i3?OKZn&${Ts!`{z$YZS1 z|Ma^u?>P4OxrBPuHjZ}T*_pKW+p^S+uRjYV0Ucx&nz@^1oo!J6@tRo`gO;8PG^#XN zYN^z!qo WpwfWdB)mreebC*42i-9%_Cezn?KD z${U2O0gThE@su-_`+c1*v7z sYSkNP#V^L!jHVo%I1KnG_aG zX>MunKIxzxr1Khxz-d2UA$G$>ua`vaW-Rf;+Ae+>zHyNG#D+ll3|ADW$BR U%DGJE4jFfQf~>lS@kCcY219Em%3@%_DE=1gL s(!a+SNo&I0j5_CCN)hgS`2)&x*u5w-GfReRg)nh&?&^_EQ5NHXLxM*6lIx@#~~ zy4x{#>6k*JcBL_iwKx1q;29%;*M9IcaKnLevh8{c(oV0PzD>Z~oH(-M1Z|*}V%=-O zz}E?No@`QXShW5e7XL(Tb9nKDa=y^?VFbrktuENm=VU<*>57p_VJTyiG8QVh=zi$w zw}U+3RSi8UrN~??WZKE;LO&MJXU%AUPYP#EMLL+SRUX+{61BswUvQ!vTy=Hw&&iHD z+*$SoAah`QozB_2ca<`PcI8h!XU5(iG_AQ3IE_u)RCuj;by(Ni>ed|>|1KTc ~mh@Y-DmYU!GRq)AYRHyZUDtr8v#BeScvabH~z?X V*bCvpAh-AF1@P45Eo+>M^I|x@y<_sbDH#PRTh!Kd_<}jh zJ|TEp0VUP8t~m1^|1#?FWh7JbyTP@(d(NHr5jtjDE-=CZuggtMLnzp<1&)8Qa$qH( zZ+-9d7$?1APP&xW)_Xm_(>wFjggJ?J%?F!r6~to+foTfg1rG+JwiDS+MFmH;$H=#? z9+n)odF#UbH|N*jKZ4v!muuU*y$mM`S9TC@4sga1Gk%`i=I&yj`Yv1v@w;17uLV;D zoYM}t9+YsVv1cwIzq;>Su17R_iSnkIzL&dF%ui6{Hnq$^m3HtRi=yBoUP==shl|B5 zYOHo59FP^rh*>)emY(WWMSlF)3F1@iQ`^ne?^naF=&_6Zih{P*zg05>e{l&T;w@TW zyyY%q!=8f3!5v$7%$<{@V{maM?gjM}s%6jT;JA$1bLt}fedU<2zW3SuUdxa|FTzt4 z3_o}%G}1Ok&T*!0xj;7RCcKJpw1ixw^b2wC!y@(b8_pH?M0m$y-K6EFEHvj=X MUK*=Wm_wye!&AG)uj#6@3b3aUIQ4$$L;Ec;*6OJJ6C!V(6 i?&FVRoW$DGZ9?SEb~+` zg{dBTCc@AwY}m=PJ2G-5`ex~f+XsB2uIbe|9ausHE6arprE+9)QXX<^`FAvVUM?a; zGmy03vNd4mza ~6d1N;R!AYui?M5*(1}hSq&BOh>QeQ(+vF}d@FMW>o4ZSB s#K)P15#?K$o!sjvyS^$mD}JpquXZJ5 z_J#qeP^F52ziq3NUk1FgXwjPGE_C(L=-lg;$m@x-vp=TWkbSAt%KpNDRG%$JhrI&& z)T4U&c3SzfT-oTHf(v7ImvRE@k1(Isq2E5`KMP5xxn=7u5sHqaSN9#C-P0I 9T;u?dUb HKqae42!94F|qweGjkSR|Q_1(IfnhQs*#y%f1I8H-q*3>a) zO$=^@nqrxSVBh{ywf@ap60Ti%Q4aMsZm&pmraO0`ou&6?YFNeKk2|;US&}397p*1W z_K*g<(Q)K+!wx=@0);BLefeO9hX2$;;Zez)RB0Eq^GmJ_I>t_mUK4`Xl2-dq32OFT z{0rt_0$~1xg3N|C(a^2D&ThGHlwdaco1ZYg@Gh8-G3Xl7^mYATQ7|c++JGw=8{Lns zm9&hnp3-BYY4AG~%falG+u77S^^B9^M (}h(`aIyQZUK;Vq7uUl=T# z!wz=OuXk2t3SbgjM@?zFmq}XfK~}z2iJmbdTM^LL$NDm>LyHqnvsKJ>jog4nEP!Ac zcE7#<7#ZOpA(2EmeA?OBIyNVqmOLhJDR|*KiHISSS~g{I3f`04VuQC?^m+ $;~z%0d)=Y6#*KHg593Bk*uZN4(eSu2xc3#?s50Dy*q< zS4vM(pm0{NO8i-C*7B1nP7cn@ncamBb`onuvsPQBe)wv98Wc`2E=c|Ocs0(~0*XK^ zZyo8dPRZEbLm;zUIxZH<5fQ3epvD(lh_A=K9ehyB4Rt@q;ux)ESF&fu0^Kw`h#x04 zW0xJtEAFpWWp;)8hS|xd&@SGqx04=xie?0+KUZX@;1etDo66-|G (f-1Fl NFAeeXDUW_PueaVh4br;0z=zyYuXqGmfUn-5D$ms)GPg oA$iq|=9@K7-1#2(S>;b)(|SMMmw+M%zbI2b#zppMzgk9L zpw3c41XLRZVRG%C?H_S0*yQSjo}45a%K8OI+&>8YOe}UMVBp~Z><-wB#Yqik<8c6x zwWhh8rdikF_PF{RaP(DnUO9NbI5$k1ZCrf{lrluT#{80Bn-30@Ev#NBH3&alR`0?f z;}aBTZH=N570J2)6>Yk;XrS*NSvhLgSs8ybp-iLU@~ApEdGvCMdo8fo{r3)IyV?A2 z;)>#ouJ>pc?|?lu%giwxZDPZ@(a}5YFeTRV#-=|{spU1NzMbg;IR&jRpMcT%J&G(8 zc `;Nvz%gIXUV5NLa+4Eyq2Ybo5BH{kQL%%I;!q18f1gHBk|)U_21o&0fPX= zWtxWgZnsLEV)ar!=H!wrEWC#?gX$zwtRyyq?#Cg=SDk+P<2F9-GRbGkqnJCigOdK& z>2n4#64+y?xKHwr8A_#_@1R}mbofVbG!Kio _V+}3Xl!sD;(`taDAEoFo36p@t!VUkGNS0)}sQv1g#^)^2sj)?fM zc$T|?4JXlxfS$)+9?_z>#CJ>%&xbkC8%SBiD(@w*YARkM2T>H2ed48at*j;3F8Vbd z)vkc7zlo&dBHUqYe%{yc(IW6n{~aFdr2)$n?~My+Wui!wFuU8{G_E$-@Y4~enlfoceXQA(a!6w)H|rL0)^PltAk3|vbo1uj2wG3p|szls+A>v(YKWu!Wadaj^Eo1 zeQ|MmY=c8A5)B@FP{)tfEY%nh$*?@xn?Z|mPCf5UjCo(3Gj`F5I!$$jyLXOpA!Ie$ zEy+2wDV>&SIF;PbfL-~e)*}jnQ?9^JT0grAc&AT-i}bHwzc%0;ZFIaTqh!}84Y%Yd z=I}&R+i^@SU*&(!=l(0lBY#WB?&oU_sV+GjddEHSAX@fEtF z$E*BfpncZF7YVV ql-320o zfJdZsVzjyB2~%(A0s-|PdMO_%8<0deE)(PR8ILs2xI0Lt+{^7bfcpGc$mxAT(%vuN zwO|SB#+QUyC842DwU*@%$p<*Tkg> O5TJNZrgW%OnNut_?60sJFF%c0L&JY=WJxe!eSSF<~gl1%>Cy# zw6}_i?n-!%2sG&Z_Tru&Mv?mna$=TQJvBS==9w3%`Je+^^30P@o}7J)ia1!Tmzxf` zH81{4jzs+E>ZLPZf&5E$;Sl!$t)V;ttq7AYuVdT`D&Bz>ZdS%^G5!ab>^Wk+gX)`; zTbiL{da3;Ohf+ch+^HkK9f0b2m1c`9KF}l0uNKu``;8K=*#)`XNU?}~z%a`nz(DKE z=ttCcRAqiq_ESj%^7Ful zq2}H=F%; VlzFG$vpNgvK4ySW4pu2b=#@jGEo;|27U+i z;}9c)|83%T&|k5L`HM(%uoaS~q6>SU#`I@+Fzm-7*O2f$CHRVQ wK zm3mTuD`~anq$x!b4W#l`9((RPHv=)l6MA~_U+e2nG+|2}+rC@j`}5Tj9qGclckwBo z-2knok3ISa!s3?)l#QVEXPU!4UL0Ug2NPbf7AmGEl {y$FKgukpP*CJwnIa=@v63{RgbyJgYk-0Q=y% za7O9BJLG@PmTvw7M*@)QBdfBH|L>IRf1I9<2n?{F3r}(G`3L%QL(CY5D2osyLj86Q z|6`)}Ux&?60@y#+=Zt*xcM-?!B mS~|3xIu@Rqpi=AOimvCC|9>9-|MT$w|MuY@aNDgf9}BFe`KR8Wj!6B+ z|LuKnuKI^UT*KR7F=!DY-RYh&6~5hwe ~Yb`ehkfnercg4)}q(&P_t z%R$GUdV8!zmjrNY#3C Kjg6UhtFLN@V8&KtzA;ORm(d zM9^+HaU$l&x|K)~P=%w7PmK6 Xg9<6$7#2CVU!--ovpU$mB=5GWmI2Xftt7!{)aKp)mY(Dv0i6q@9n@eIG z$?eMK3J{6*{mdtFD-o?5Wh>Q8IJ%vP7C?m9+nVp?twbK(DBC)D($wwBCUnCINP&;x zRw8LPsf?wb<>-U|FVAQk@QgloYv$L!^*XXQHxO4d!1UG|I1mb0N0(pf+iAC^1?(lj zb-oOajo$i=4^=mu`268_yOopwWI;_7=rk+zRN5q+`CX(pzFl7>i?jnvNbe@jaz5 KQ*AP`|u`|Sp4kRyJ(h({SEwB!-U+owpZjow*Kg; zJj;%JywEXe*|}K(dzPQFVo_L5^CvJ($ZNn-gz;Z~Rx<3{%-pp&C6D#v(=~ad833E! zAiRI#au_yO-z1)5Fe@KPEzAOMKXSMu6_LW7zd$--KOh1OYSehoIV`j}=dx+n@mYx( zR06XEb#bc9N`qg~FrIL8a~q|~|8@2d`~cw9Hg8kC7eBkbUKf4LicRIdY8eHFDIb{IdnZntDX6Wb4Iiak`7;B9gCtdbbp;;F@x zi^q v z-hyv?2jf2;=Fx~cp!?)*jCXxPJEDah6+L9pn>PU7`o?vm8UGi1ZypbI`^67OA}T55 zE<~$PB(j^)MjKOv>_w7&H fT~Y~^E$gT($xe1=NC+`9B
_P53|2Grk6aulKHY9ffw}_uH)og>@h@UAfu|N^G!|s5~Bm9 zbm~WI4mJ*%aK xv!yCE=a=d94gyB%W^f%QfoJw6<1Os-$hT+9o4QK zM_r+sloVFWz-~3#8!sTlkPC(WsZEyP$bR3E(J!-MrvpKi3q4{CM5$@WZRTIM(2-w& zpOl$(8b2Z0Jzrn%D`9s>?K<6aUZT*c<@1fA=P~y+BP=x1?iBd+l-lM!-V1tXdjYAt z&9?1B_>7ukH0`2+lz-mC`%7B9qPMe#DvWfqEp%q>iJ9jw<2g&W)-hh>$i2id(OSK~ z8X~G<9n(b*H`GU8Kelrt8L~XN8*2 $b1q}KQ{vo8 zep1!MCFj(GE5{oGo;r%U(3UToZbNQ>mlPI#CoO{ Ie 5r77Ms`_r0zlIM$V z4eh6nx8zk3R5uRnMO=9ldt@JQXGvu33YT)E@An<@U>_Sa5M f znN+ktopd9A0~{c^C9l6unMut@IDN|?oZANCdZhUnAn78WZfUYx7m=Qg048Lj+)!k) zUhspc(Uo|0N~XpG{YL#;a|9=L-Xo~n9+{se(cXR?5!PwTVyb;Z)mv *Smz8TT{DYclb XeUTITHH5^d&?kxn;l?ZW`w2v5v)8q&cwQRI4Qd>7kU6P0zfV_>*uk6Y-GJ5-{ zx@0#}H?~XKsKQqMJ7>kWk%6Qa_ooY{V0twQPIB;l-~sfp@rw_NOgj3vw*LO mkFcm>(M$4y?dXP_Fh^6iJebg4jvo*bf$2$ z)f?0jujiU>vf}ya%|Zqwz4@ilNr7?UUDDTc?{j~#lnUNCb>0lNTcyIITBN7oB5(Hc zRbA0bi+#z&J44Oe(yj#eF8XYV%C)jh-`jhrDPdO=s@}3`lE1N9S|GFcI!|=oAwd_! zdtL0vfSu}wJ6Ibp^@5o#dM{VKyn8!PqKBX;cD?MjlX|J{8=IWk IT)xhZD>H0^7UZmT`6+REAd&{w*@r@j4!t+3Aocmbn z+sxFxVwTr*lT?q CzU3_eNV{A-r*XG+zA|E8SG|2S8(; z&VBP@zS-c-dE+7+U`#)>r6v_R5e$t{x9l`G4D_5wmBBj7?DR9Qmp>DbwK%PttSaf> zN}Z%m_%%i#>4>k{9;oj4VuuS^iqM|=aZH@FN5FEEt)PTi9>4ZBVP|vLuHy%CT4^Ug zdcWPY @D`Exqfjv_Yn$1SWy*(e58$wP3&Ke?HEAoMKHn zyUVF5BoerIRj2T4DGt!{<-{#r4xgX!aC5u&L>qk>BUiqEheMRsRZKsPI?$0#9n(wE zWK+4FJUyR4MeWEwY2Wc>Z>-0%-ztq`_X>({S{ lw5}#GqncRCs^ta}H8e `UI9n< CP`PJPYYX)!iGubjgC>5n6f*PVc*7k)!H0I^O5}6z1kO+XJc#;oTc` zK}_d(?b2Nkw%E#_t}B{$Yf=w{O?ryW&~38S2S+bBbgM*8JM7?}cIg}1D|xk zG~O_lNcyPnO)le}+%^lYi5nyJzP97H`!s)B&1sIf<?13RmE%rDi?Gu@$^$;lPFdt!oWXB<8xlV|=or#Qxfpy=aOU6|{q ^&6O5Js7Nn}HLW4ag4>IJS#e zlns5qycUcMh(AEOtgW$!y?}AdYM6HvAH3cv1z!PR1r98BxZc=tB*kFB0qWnkw`VX| z-iAb0s@C3Zl%1&HvwTJk JMfSpY|?pU ;(A-G%O^+TK zK7#fF8oDKBH!9%Xc1mJDo1ZOxwp;M7*onp|Znd(EALcDE5Ur`q=C|fFu_U8E3GaXX zDgB)jclp{2uIkV`H!f#&A?3PkisP#|N0Umtu(#tB5P!={nOt2N0!3V8)9?Ch7osyp z<(JT_zUiqpz|qj~c)7;esBuf26R4BEqsitobeuR))Oc^H?Kw|rT3hf)tKWe^VJRS= zt1xtOb+7NRoJ;%KS^GCRsiv=8_qn85U^vg+-}={)J>i!i*eQC1osAEcycB}Q_hstb z4WPsYR+qe$YIHaU+cs#|S7MWOS%mUb)O)R63RU`cQGHTddJ>^T5T-cZ{|;DRnlVI{ zb}ObNiA?Elr-)XA3D~P|Cs$lkCIvEPp0&g`iJA7_2w7-+8YQ5TAAt64zGNY3{3Z%w ztGexh?9ze~vGB#;^Dol @CnmPK{Uqr6vvLsTtU+(2DLZxGIG9DPy{ VMVCCa7n^HvT-u^=^gOkG=CvtXKPR4+| zVXYIkhkUVC{6}T?hpmwxCdE|wKFr>|@EA(nle8*5cQT=G?jO&^yUb^WkZRH=G@XCH zzdM{7nz;czSefh(Ho0KYt>H11nq-;%Ia;rJBw#w9v(G33c2O6pJZ^53h3$s> W5ihb@=VJCa@;5&?fWFre$^NmP@tsCl5_>K{7Vs%Y1&gcj8f+ zx7o?0%U$|8x~8uTkM+Do+!b~9>Q2%t(sin=EY33Qo0^%0cRO6MG%2YVP_wH^Z~^jy z4r0BNxF=4YJf-2Z#}E5x&SZz7>FP3dEV(VIs>{06vH##6AFQ_#z2w>T>I%_8G!)4_ z7D2f`JpVk S0GBD(sRvB4%GP@S#WS1v`0`xT7hAH^HB>nNAkAHD8lxxty^45Toyq z4EP3$oAV&)yKg7+jX2k`ll{VbFJR=p6giao{@vySAPn(WI>fq}*_k6Y&3>vFx^UT< zZxze?NoRM@+;Q#Y7%nysz=m~ E5T{;)27SO@|QY gSIU?Ecih^kc18drS}b06)cY*b=B-8O|wSjP3~ocf=I!*+6n(_@kzRU83wHORY0Qdw9DQt-kTOoLkyk?RW8$ zRWf}gDngUzEA zGr1|7988SYhKjlZu{@1xH#JXAQfLXaiuFGjh$Di49Ll@%tWv~s_UCnt7O$+f $~jb4k3R(cnN-f=ZUt`6fI+xz(%jPK3y{_Vo@!n@iE=mKvdV3TW`$JZ;hSew^$($2ehYw#&;GLO~A!n-I zrEmY{z3j1=#R=3eER%j$xsmLzleWBFn`6&Z1$B!+eggf*#BtLBlxN(Dj&oUq2|y@0 zQppnJn}R>{vhi)7S;C}#dan&NNHd!uU&YeR)b-`73Xrb{=(8d>?r}#|zPWp_e yg2{my{wVfEPVYw1YmtWy7XV;J8E)uH=)^=9M!XUg>FGy<9~>Y*u{)to4K`Zk1* zY~IQ)L&F^tc93A>9C0+jIt|X0am~lRIl3Sw>MSv1sc5Q*n12NIM(saz$P#9(QV36j z#>=NP8n0I5>aFg91R_YbE=_}* m9dq{@>dpe(dV1AlzKizpYe;| zu*TUmtx~>`Opgta5SBnsPlx|=vUB1fpED4lo8<7i4{i4#+vnlVlE3M>Nop&$m)Q9E z2<>S_QAZ3q+JsL|!+X$}B2kyutsI|2w$1185FB6UX1e<~Jf6OIv5;`Gnygnoo4(6? z5n!=KTQHb`Iv|CtOEjjtJ>@>&1sNz9%xK`WJ$L84sr0Em&F$T<3a*}+_3ynDZU4}F zE)Rs1Y>AawFzzyc0lFTIupK%btP_ZWxTyI})UfTAyY*bY1>a?Eu%y91?eS)qcJ%#s zysdeS=axYr3Sj-_Q!-O?XC|H=IB}~+GcTm_Zak>TrjwqG%kpA_?+
7dy97Hie;+t|8VWqTAnZ4i$ zV7_ Z_YJ8~TJ1ZRtrRNxoG4+kS|!C%}{^)A11ie5H6 zmU<;vRWHR;(3A~*Bks-M#U<}^Bx^v-5;m;h+wN3sBxh-ZBqdE_{kuIlr3RNDL{+~` z67_n6w_Ria&0_sCdw#e=LktZ@r1T2g&FbE@9OIWd54iq{K1OdmB|pJc1Pzm-8UFR( z-{`4ln|8Xdo!Q>oWO`ZIah?N$SFtU>k_WJLCcz8mFJ5dvw }jKflBYD zFHc{=N=6=g`Y)a8vpdz;`1mhLe{Z7O?YT#cndTc~pWRT+nU$d%AC)VwikSAPSs{S* zKUGC#I_9==+Y`REmJaS;z;L#qsq@mtg|1I^7wt^us`SP#mPl1|PF;$TvPnM7FAE_4 z9>EmoqS4D+7yK`q #(#eeo1 p*l02Xl@}%p_ZD3c3oFHK zuM+myHUUizr}(~EAiyn&YrXw!w1*?0!c#uo_TJxkN!Q}1#wQhe6Ej99z9r4f#@L^< zpZHpSJ6W-)rD{8HRf?o^%rSYKe|Ta-BOkjQLx?zKr83qRq8^42(P^%pPbJ$&%OQuL zW~WpNt&6k$&=J=v(jubDtHLzC*le7Q?A(|u_4e?!Zh-QJy}=q);?GH1;J15IgV&&y zi7H<2cw;L@6*D`UuUWKcs(GoO+l8L(Da}m}T6lEK|I&;3Wbnm|;!M|# 4QS_Q~ELS9Y?5p1#cu-0D810BW?+gH=4o=QA>Q zZUb&gj(~q8_cuH2#c!6F<#)VDVxvZqTHM9?=(f`57#K8PM|v1K<%blIe^Hsz%F(6Z zql6C;yW7UASfHJktk-#ur+C{*K}5v!i|E<(o2$tYzEUk2UB)RFyWY4%8~vx>nD`pr zh1Qx>hKr^$0Nv*(ZhjkVLs;!D>+kZcDZo> myd3vF} MfauFTE9A&>*K2u=dKx3HNzcH9#POxop&A< z)-uL8St8p@+mWXaHklr`&@0J#(sdyDn~X`}T&vzf8-99euG)0$TY8PX&tNcy_fDWi z)>j8 3f77Wq&hpXlos0TbZ@mRCZA8nI4r+5=;dfG%U6aqPvc8CQ7y%Ly8 e{V%}Qo4bGElhR5R``WhuXmZYl!Re%QSn9kka(@anpdgIz+rUjTD>ih7HE)a zQO5~D{P;UH1Lz!!o8fjQwD2bL9f&VAr+`DHVYG_xd^xzs&IE3`?eF4>hS8X!!*&L5 zT)Hz~U3;U~ QfmSvRlY;hbEr3Tr{Eujhi;Q8Uw$v%=x zQib BX4=!a$C{W)==Af79QIYmpec{XWViWPpt}bXZet+W(>nkgoH e-OPAyn>mKje7Id8 r=rHOVn?xe#jM$GJxxq&55~ zx;S+2V7_L_#|ha+iqnE|@v}@_%XI31cW=z@m0K7WJi9x?my{lJ_=>3a-%iht{j#`8 z^6HwZ+OY$I6Y~DL920PvjXw@7 dr7NZKemy1bE49IkFxCt ?4kK zQ%N(fw-Ex8CfBAuXW5pTmfhJ=DVfvUw-#2~QEUvo?%dDy%BEYlTi>V6xi)i=Ag1U^ z{HmotXSC6y7bsX4c650LN^vB*8jTosEZJ=l{6dlW_}%7fUgHKt#{yE-Wex=jv=-;o zjz%2*kk%?IpVauKZ!V(oapX*2NzcudA%yDV=TB&qB6Rej&|Q7ss*9c&zV7yqFTtj} zuc)~s6RHM#?h#v}7M5i|`7U6Ur&GSoiT*S`QfkaA1jPQ6Q@PqJ01`$#mm*SiilI*a zP-09R796WF-ZkJ{Sp*zVI!KKtD0Zu#y1m+utS(|41eH8P Q+)yT3k %M)_X-dcx1 z6WKVd&H=&M*=85oHvT;8{?|E*rno&LYNs`ASo6SnaZ8w;-O@dYB?N%5ODd}N4$`<~ zn`^r~*uxcMP>&UStg@33cV=J@^LqMu^F%uI!)JTHnuDNb`|RfXuc_rmkHw+}Y7KKJ zGOpg@0h!HuMWihE{l0l3gJqiDANTI_<=GY8fYB|v 4Cx-Bis+(@3zXzX6H%v)wqvIU(<%Tj9b^ZWH_}$QXop5xep3l CoX@dC|m()YK0zH*K*gpb{&DO&@R$DlCC}IjGx3)hCLa z2djL?K1qm`8hCkW`PJE(T(Fd$a!635@*BHI(P*W-NyKQLXsO<&7bRKr@D?OaX5`uw z1s5I;1~@uNv-kXD212Xdb@PYXisHHD6^JsU&UZ|%(zEY_1k<&0;uk^C>4~ep#3U#@ zYE~iWoBcQ_(`hxa`~;;D2^%MzPiu}&8>kkYAp PL26NI4 z92~eRn-lJ2?yr7BH72@{&_=flUv^yeh$o4jMQ`5 ppUD5F-d~Xfha`X{qKhu3aHmGN~@!BiO<6#X`%z( zIsNWw6$n?bWk9m6>ao?YNsSxW3-&Vz-rR%upmibjNdOJK8rO46*>Sl2TV}*T%N-c8 zH%Vtq$!N7v@ ^$V946xkZ}3f(Sc`r(l?el+Voqq{cURUQEX zF7w@(gU~_cAEp)_0aX#*tOQgBy6>-BA;acpkn%l_?^dv;Atz92be^Z27SY{Xr&_ia z_*g{s=vThJA69Ka7aGzy5Z!rqQ|(XxnvlL1EOYnr2ULn-wGx}V#4RQ3UQgp{t28KG z^$NhbPJYVD7?lh1aYE`)JFh)4KlTs9-+7_XZN)E6^%HioP4_r>c_aBQZ85g9^O~*q zhi>W1S1hGR9JYAxRW;9Q@Sf-}=CWBEgwMa==1W#Im`yV5=J@6!cM9et37klMNvUni z4m6V3I+6QaIG>OI$7zC|c-BVpqjs$NF)bo;c98(Y9$=xkN{JqNl>Qj3>D{^c)>upG zM~sWAuU(ptt?B9uZ)ln?bdw+S3v8vgIK8Mx068=GsU&Rjt07jcmAgImsKLbw&{Cjn zMdpJ%+{R^MORNPVjuA#M=(R$u(>=a+uN=8uf#L<-vMU7|0O_z{oN!EEzUZEJ*rp{q z(!bJcLDwTW_Z9!UZS6`wUD9B)QblylR3v?7l@8hpNK^xx|@7O zd(e(0(~0tf*UBtqH??#<8TQJ3I^XU0hS(F_8j*@@=$%?<^Ldp4j;RU`?fW! cp-L;vc^UVY;M zJM`LoRt`D&S>jQHtV0)zT4X7P=Fk{7qdT(Z>5o0-1jmy~+ @J@PN(m5>bn8nL7ZUNA(*q(Ch5SW7B<@T}41e-^3 zHpMf2McTLFbI~H|KAL+J>_>CFvq-4ttL52dC^N2&@oEo{hBp0yf YF)mMhzfe2;Ddpo5xEwgk*%U5#Mst7sPD=w3hEmQC8uxl@Sdhptt(WH!xx`7rU zNxIgmRlu=#9K8-wXifL_N9T5@1WjOF`*oyC12jlmh^d1RjeQI!cd6GP%LX@CUFb7( z`oaa>r#FC-UsurE73_Q87j5J3@4mP!9h-Oji0w4LZRaWXu|m><8EP4J-`1EU-@gH# zsG qjp;KVx~KTs3LD4 >rRL~hs+=MzA1oyB8|IrjLENBz&K zW%`f~wB7m8?KP4ddxp>!%+XOa_;e;K^~!0{%GRi3muwceApWJ6z==UB5Ft}NqMAP! zxfT!9BZ~v-iFkK-I94RCsq9EG68vG63>jeBBBD_=W{VeW&udw;)CWn`6Q#JZK4YAZ zP<6w#eDud_g^;Y7FD2%OAMYj l{oe}3EG_BWrX^UV=x5O9E zNd!9Th;c`?NzJz@LFro!Z!xg+5P(1iu0i-a!CAv7rN*`{cL)4Lp!eR?eSD#SZnB ^HR-#=fsyJG`m+Am>q_xcWm!x_y3*9}%p7cS_KCVqZDQPpVag1UX2IK8B>k z-p#_Fzbnyj5eR8fbNIA2`veH2>$$0sVM0M40-EwLEJ|X3>!JBGjE)Q(M)_q8`U@#+ z+(YYLaWFlZT`F?H#5yENY$!2OO%siH7rMBVH1mVc3Dnf$c*otIVUP(5SY39%MCeO1 z3ERUK;x_U16csk5a6uPzbo&CLvkm<0nbn&?-g)B^_h_k-6HY;YZ-S>goOfBpkj`dd zkaB=KlVBLqWnW(VH^L-oMVS8y*4nkyEZu6l>+svWXfuzowwYub>6Y2SYE$4S`j#}7 z)^^!{I>xZ{(g5ee%xV5cxWEaRn3$N$8M1CWrsqvEA&)fPk|>7oJiH0MU|68EcjMr! z?MC`3p cxYcL&<95>5UqA7j^hOIPcFiW0cz&`X2|Sl| zs)w3uORu0EW;0iF$|v)^>${9LdBU)5?xDMmMv2`HAfPW+NVVg A0Ip| zw59ANdQW}MljsKc%{v+DYw5Tb>5w?7-hwChHM4=h|A;Q>7T *l?;8j*j40b&Urye zN#89(vB0tU 4`El1swJ9}(hTt+?+zL0#@COlNnLQ^|KhPxqJ0 z*A2}A9!utvC*r|8LOXN9E1{Udlv>|4kIqNsdw7oCI_*whoQ(v^>V0_F$-f%9>9K=y zz8Tg1nHqF&Ph8ZkMW{^8uKaJlhXrJ>p03bXPBFh&N7ui+8Jv@T>fNLC{qUztFAKXl z@Cs#lK~-<#{ly5~xzTo!8L7p}q#qGwn@v+*Pku9a<7wyNa_r9RQ#E<72v4!I&RAa^ zV$a-M{pB&5&g5)