from __future__ import annotations import json import re from collections.abc import Callable from dataclasses import asdict, dataclass, field from typing import Any, Literal try: from .ckan_support import DEFAULT_CKAN_ENDPOINT, group_list, normalize_ckan_base_url, organization_list, package_search, package_show, tag_search from .model_roles import ModelResponse, call_role_model from .openui_support import OpenUIValidationError, _json_arg, parse_openui_lang except ImportError: from ckan_support import DEFAULT_CKAN_ENDPOINT, group_list, normalize_ckan_base_url, organization_list, package_search, package_show, tag_search from model_roles import ModelResponse, call_role_model from openui_support import OpenUIValidationError, _json_arg, parse_openui_lang AgentStatus = Literal["selected", "finished", "needs_clarification", "max_tool_calls", "error"] AgentEventType = Literal["model_action", "tool_call", "tool_result", "selection", "retry", "error", "done"] ALLOWED_ACTIONS = {"tag_search", "group_list", "organization_list", "package_search", "package_show", "select_resource", "finish", "ask_clarification"} MAX_TOOL_CALLS = 8 MIN_SELECTION_CONFIDENCE = 0.85 CKAN_ACTION_CONTRACT = ( "You are the smolnalysis CKAN retrieval specialist. Choose the next action to find the correct dataset/resource. " "Return exactly one JSON object and nothing else. No markdown, no explanation outside JSON. Shape: " '{"action":"tag_search|group_list|organization_list|package_search|package_show|select_resource|finish|ask_clarification","args":{},"reason":"short reason","confidence":0.0}. ' "Use tag_search first when the user's terms may not match the catalog language. " "Use group_list or organization_list to discover catalog vocabulary when package_search returns weak or empty results. " "Use package_show only with observed package_id values. Use select_resource only with observed resource_id values. " "Only select_resource when the resource directly answers the user request; set confidence >= 0.85 and put a concrete match_evidence string in args. " "If candidates are broad or only loosely related, continue searching or ask_clarification instead of selecting. " "Do not invent URLs." ) OPENUI_CONTRACT = "You translate structured smolnalysis results into OpenUI-Lang. Output OpenUI-Lang only. No markdown, no prose." @dataclass class AgentAction: action: str args: dict[str, Any] = field(default_factory=dict) reason: str = "" confidence: float = 0.0 source: str = "model" @dataclass class AgentEvent: type: AgentEventType detail: str data: dict[str, Any] = field(default_factory=dict) @dataclass class RetrievalResource: package_id: str package_title: str resource_id: str name: str format: str url: str @dataclass class ToolResult: tool: str ok: bool summary: str data: dict[str, Any] = field(default_factory=dict) error: str = "" @dataclass class AgentSession: prompt: str endpoint: str messages: list[dict[str, str]] = field(default_factory=list) packages: dict[str, dict[str, Any]] = field(default_factory=dict) resources: dict[str, RetrievalResource] = field(default_factory=dict) events: list[AgentEvent] = field(default_factory=list) tool_calls: int = 0 searched_queries: list[str] = field(default_factory=list) failed_searches: dict[str, int] = field(default_factory=dict) searched_tag_queries: list[str] = field(default_factory=list) consecutive_service_errors: int = 0 discovered_tags: list[str] = field(default_factory=list) discovered_groups: list[dict[str, Any]] = field(default_factory=list) discovered_organizations: list[dict[str, Any]] = field(default_factory=list) catalog_tools_run: set[str] = field(default_factory=set) @dataclass class AgentResult: status: AgentStatus endpoint: str prompt: str messages: list[dict[str, str]] events: list[AgentEvent] packages: list[dict[str, Any]] resources: list[RetrievalResource] selected_package: dict[str, Any] | None = None selected_resource: RetrievalResource | None = None confidence: float = 0.0 clarification: str = "" def run_ckan_agent( prompt: str, endpoint: str | None = None, history: list[dict[str, str]] | None = None, on_event: Callable[[AgentEvent], None] | None = None, model_caller: Callable[[str, list[dict[str, str]], str], ModelResponse] = call_role_model, max_tool_calls: int = MAX_TOOL_CALLS, ) -> AgentResult: prompt = _clean_prompt(prompt) session = AgentSession( prompt=prompt.strip() or "Find a dataset", endpoint=_normalize_endpoint_or_default(endpoint), messages=_initial_messages(prompt, endpoint, history), ) while session.tool_calls < max_tool_calls: action = _next_model_action(session, model_caller, on_event) valid_action, error = validate_action(action, session) if error: _record_event(session, AgentEvent("retry", error, {"action": asdict(action)}), on_event) _append_validation_error(session, action, error) valid_action = fallback_action(session) _record_event(session, AgentEvent("model_action", f"{valid_action.action}: {valid_action.reason}", {"action": asdict(valid_action)}), on_event) if valid_action.action == "ask_clarification": return _finish(session, "needs_clarification", clarification=str(valid_action.args.get("question") or valid_action.reason)) if valid_action.action == "finish": return _finish(session, "finished", confidence=valid_action.confidence) if valid_action.action == "select_resource": resource_id = str(valid_action.args.get("resource_id", "")) resource = session.resources.get(resource_id) package = session.packages.get(resource.package_id) if resource else None if resource: _record_event(session, AgentEvent("selection", f"Selected {resource.name} from {resource.package_title}.", {"resource": asdict(resource)}), on_event) return _finish(session, "selected", selected_package=package, selected_resource=resource, confidence=valid_action.confidence or _score_resource(resource, session.prompt)) _record_event(session, AgentEvent("retry", "Model selected an unobserved resource; falling back.", {"resource_id": resource_id}), on_event) continue result = execute_action(valid_action, session, on_event) session.tool_calls += 1 _append_tool_result(session, valid_action, result) _record_event(session, AgentEvent("tool_result", result.summary, {"tool_result": asdict(result)}), on_event) if _should_stop_for_tool_failure(session, result): _record_event(session, AgentEvent("error", result.summary, {"tool_result": asdict(result)}), on_event) return _finish(session, "error", clarification=result.summary) auto_resource = _best_observed_resource(session) if auto_resource and _score_resource(auto_resource, session.prompt) >= 0.86: _record_event(session, AgentEvent("selection", f"Selected {auto_resource.name} from {auto_resource.package_title}.", {"resource": asdict(auto_resource)}), on_event) return _finish( session, "selected", selected_package=session.packages.get(auto_resource.package_id), selected_resource=auto_resource, confidence=_score_resource(auto_resource, session.prompt), ) return _finish(session, "max_tool_calls") def parse_agent_action(raw: str) -> AgentAction: text = raw.strip() try: payload = json.loads(text) except json.JSONDecodeError as exc: payload = _first_json_object(text) if payload is None: raise ValueError("Model did not return JSON.") from exc if not isinstance(payload, dict): raise ValueError("Model action must be a JSON object.") return _agent_action_from_payload(payload) def _agent_action_from_payload(payload: dict[str, Any]) -> AgentAction: action = str(payload.get("action", "")).strip() args = payload.get("args") if isinstance(payload.get("args"), dict) else {} reason = str(payload.get("reason", "")).strip() try: confidence = float(payload.get("confidence", 0.0)) except (TypeError, ValueError): confidence = 0.0 return AgentAction(action, args, reason, max(0.0, min(confidence, 1.0))) def _first_json_object(text: str) -> dict[str, Any] | None: decoder = json.JSONDecoder() for match in re.finditer(r"\{", text): try: payload, _index = decoder.raw_decode(text[match.start() :]) except json.JSONDecodeError: continue if isinstance(payload, dict): return payload return None def validate_action(action: AgentAction, session: AgentSession) -> tuple[AgentAction, str]: if action.action not in ALLOWED_ACTIONS: return action, f"Unknown action '{action.action}'." if action.action == "package_search": query = str(action.args.get("query", "")).strip() if not query or "://" in query: return action, "package_search requires a plain query string." if session.failed_searches.get(query, 0) > 0: return action, f"package_search query already failed in this run: {query}" rows = int(action.args.get("rows", 5) or 5) action.args["rows"] = max(1, min(rows, 10)) if action.action == "tag_search": query = str(action.args.get("query", "")).strip() if not query or "://" in query: return action, "tag_search requires a plain query string." if query in session.searched_tag_queries: return action, f"tag_search query already ran in this run: {query}" rows = int(action.args.get("rows", 10) or 10) action.args["rows"] = max(1, min(rows, 25)) if action.action in {"group_list", "organization_list"}: if action.action in session.catalog_tools_run: return action, f"{action.action} already ran in this run; choose tag_search, package_search, package_show, or ask_clarification instead." rows = int(action.args.get("rows", 10) or 10) action.args["rows"] = max(1, min(rows, 25)) if action.action == "package_show": package_id = str(action.args.get("package_id", "")).strip() if package_id not in session.packages: return action, f"package_show package_id was not observed: {package_id}" if session.packages[package_id].get("_shown"): return action, f"package_show package_id was already inspected: {package_id}" if action.action == "select_resource": resource_id = str(action.args.get("resource_id", "")).strip() if resource_id not in session.resources: return action, f"select_resource resource_id was not observed: {resource_id}" evidence = str(action.args.get("match_evidence", "")).strip() if action.confidence < MIN_SELECTION_CONFIDENCE: return action, f"select_resource confidence is too low: {action.confidence:.2f}" if len(evidence) < 12: return action, "select_resource requires concrete match_evidence in args." return action, "" def fallback_action(session: AgentSession) -> AgentAction: if session.consecutive_service_errors >= 2: return AgentAction("finish", {}, "CKAN service is currently unavailable; stop instead of repeating failing calls.", 0.0, "fallback") for package_id, package in session.packages.items(): if package_id and not package.get("_shown"): return AgentAction("package_show", {"package_id": package_id}, "Inspect the next observed package.", 0.45, "fallback") best_resource = _best_observed_resource(session) if best_resource: return AgentAction( "select_resource", { "resource_id": best_resource.resource_id, "match_evidence": f"Observed resource '{best_resource.name}' belongs to matching package '{best_resource.package_title}'.", }, "Select the best observed resource instead of repeating inspection.", max(MIN_SELECTION_CONFIDENCE, _score_resource(best_resource, session.prompt)), "fallback", ) if "tag_search" not in session.catalog_tools_run: query = _catalog_query(session.prompt) return AgentAction("tag_search", {"query": query, "rows": 10}, "Discover matching CKAN tags before package search.", 0.45, "fallback") if "group_list" not in session.catalog_tools_run: return AgentAction("group_list", {"rows": 15}, "Inspect catalog groups for topic vocabulary.", 0.35, "fallback") query = _fallback_query(session.prompt, session.searched_queries, session.discovered_tags, session.discovered_groups) return AgentAction("package_search", {"query": query, "rows": 5}, "Fallback search query.", 0.35, "fallback") def execute_action(action: AgentAction, session: AgentSession, on_event: Callable[[AgentEvent], None] | None = None) -> ToolResult: if action.action == "tag_search": query = str(action.args.get("query", "")).strip() rows = int(action.args.get("rows", 10) or 10) session.catalog_tools_run.add("tag_search") if query not in session.searched_tag_queries: session.searched_tag_queries.append(query) _record_event(session, AgentEvent("tool_call", f"tag_search: {query}", {"query": query, "rows": rows}), on_event) try: result = tag_search(session.endpoint, query, rows=rows) except Exception as exc: return ToolResult("tag_search", False, f"tag_search failed for '{query}': {_short_error(exc)}", error=_short_error(exc)) tags = _names_from_ckan_list_result(result) for tag in tags: if tag not in session.discovered_tags: session.discovered_tags.append(tag) return ToolResult("tag_search", True, f"Found {len(tags)} tags matching '{query}'.", {"query": query, "tags": tags[:12]}) if action.action == "group_list": rows = int(action.args.get("rows", 10) or 10) session.catalog_tools_run.add("group_list") _record_event(session, AgentEvent("tool_call", f"group_list: {rows}", {"rows": rows}), on_event) try: result = group_list(session.endpoint, rows=rows) except Exception as exc: return ToolResult("group_list", False, f"group_list failed: {_short_error(exc)}", error=_short_error(exc)) groups = _dicts_from_ckan_list_result(result) session.discovered_groups = groups return ToolResult("group_list", True, f"Found {len(groups)} catalog groups.", {"groups": [_compact_named_item(group) for group in groups[:12]]}) if action.action == "organization_list": rows = int(action.args.get("rows", 10) or 10) session.catalog_tools_run.add("organization_list") _record_event(session, AgentEvent("tool_call", f"organization_list: {rows}", {"rows": rows}), on_event) try: result = organization_list(session.endpoint, rows=rows) except Exception as exc: return ToolResult("organization_list", False, f"organization_list failed: {_short_error(exc)}", error=_short_error(exc)) organizations = _dicts_from_ckan_list_result(result) session.discovered_organizations = organizations return ToolResult("organization_list", True, f"Found {len(organizations)} organizations.", {"organizations": [_compact_named_item(org) for org in organizations[:12]]}) if action.action == "package_search": query = str(action.args.get("query", "")).strip() rows = int(action.args.get("rows", 5) or 5) if query not in session.searched_queries: session.searched_queries.append(query) _record_event(session, AgentEvent("tool_call", f"package_search: {query}", {"query": query, "rows": rows}), on_event) try: result = package_search(session.endpoint, query, rows=rows) except Exception as exc: short_error = _short_error(exc) session.failed_searches[query] = session.failed_searches.get(query, 0) + 1 if _is_service_unavailable_error(short_error): session.consecutive_service_errors += 1 return ToolResult("package_search", False, f"package_search failed for '{query}': {_short_error(exc)}", error=_short_error(exc)) session.consecutive_service_errors = 0 packages = [package for package in result.get("results", []) if isinstance(package, dict)] for package in packages: package_id = _package_id(package) if package_id: session.packages[package_id] = package return ToolResult("package_search", True, f"Found {len(packages)} package candidates for '{query}'.", {"query": query, "packages": [_compact_package(package) for package in packages]}) if action.action == "package_show": package_id = str(action.args.get("package_id", "")).strip() _record_event(session, AgentEvent("tool_call", f"package_show: {package_id}", {"package_id": package_id}), on_event) try: package = package_show(session.endpoint, package_id) except Exception as exc: return ToolResult("package_show", False, f"package_show failed for '{package_id}': {_short_error(exc)}", error=_short_error(exc)) package["_shown"] = True session.packages[package_id] = package resources = _resources_from_package(package) for resource in resources: session.resources[resource.resource_id] = resource return ToolResult("package_show", True, f"Inspected {package.get('title') or package_id}: {len(resources)} resources.", {"package": _compact_package(package), "resources": [asdict(resource) for resource in resources]}) return ToolResult(action.action, False, f"Action {action.action} is not executable as a tool.") def build_retrieval_openui(result: AgentResult) -> str: event_ids = [] lines = ['root = Card([header, progress, candidates, callout, followups])'] lines.append('header = CardHeader("Dataset search", "CKAN retrieval result")') for index, event in enumerate(result.events[:12]): ident = f"event{index + 1}" event_ids.append(ident) lines.append(f"{ident} = ListItem({_json_arg(event.type)}, {_json_arg(event.detail)})") if not event_ids: lines.append('event1 = ListItem("start", "No retrieval steps were recorded.")') event_ids.append("event1") lines.append(f"progress = ListBlock([{', '.join(event_ids)}], \"number\")") rows = [_compact_package(package) for package in result.packages[:8]] if not rows: rows = [{"title": "No candidates", "name": result.prompt, "resources": 0}] columns = list(rows[0].keys())[:4] for index, column in enumerate(columns): lines.append(f"col{index + 1} = Col({_json_arg(column)}, {_json_arg([row.get(column) for row in rows])}, \"string\")") lines.append(f"candidates = Table([{', '.join(f'col{index + 1}' for index in range(len(columns)))}])") if result.selected_resource: message = f"Selected {result.selected_resource.name} from {result.selected_resource.package_title}." elif result.clarification: message = result.clarification else: message = "No final resource selected yet." lines.append(f'callout = Callout("info", "Result", {_json_arg(message)})') lines.extend( [ 'f1 = FollowUpItem("Try another CKAN search")', 'f2 = FollowUpItem("Inspect the selected dataset")', 'followups = FollowUpBlock([f1, f2])', ] ) return "\n".join(lines) def generate_openui_for_result( result: AgentResult, model_caller: Callable[[str, list[dict[str, str]], str], ModelResponse] = call_role_model, ) -> str: payload = { "status": result.status, "selected_package": result.selected_package, "selected_resource": asdict(result.selected_resource) if result.selected_resource else None, "events": [asdict(event) for event in result.events], } messages = [{"role": "user", "content": json.dumps(payload, ensure_ascii=False, default=str)}] first_content = "" try: response = model_caller("openui_translator", messages, OPENUI_CONTRACT) first_content = response.content parse_openui_lang(response.content) return response.content except Exception: try: repair_messages = [*messages, {"role": "assistant", "content": first_content}, {"role": "user", "content": "Repair this as valid OpenUI-Lang only."}] repaired = model_caller("openui_translator", repair_messages, OPENUI_CONTRACT) parse_openui_lang(repaired.content) return repaired.content except Exception: fallback = build_retrieval_openui(result) parse_openui_lang(fallback) return fallback def _next_model_action( session: AgentSession, model_caller: Callable[[str, list[dict[str, str]], str], ModelResponse], on_event: Callable[[AgentEvent], None] | None, ) -> AgentAction: try: response = model_caller("ckan_retrieval", session.messages, CKAN_ACTION_CONTRACT) action = parse_agent_action(response.content) session.messages.append({"role": "assistant", "content": json.dumps(asdict(action), ensure_ascii=False)}) return action except Exception as exc: _record_event(session, AgentEvent("retry", f"Model action failed: {_short_error(exc)}"), on_event) return fallback_action(session) def _initial_messages(prompt: str, endpoint: str | None, history: list[dict[str, str]] | None) -> list[dict[str, str]]: messages = [] messages.append( { "role": "user", "content": f"User request: {prompt.strip() or 'Find a dataset'}\nCKAN endpoint: {_normalize_endpoint_or_default(endpoint)}", } ) return messages def _append_tool_result(session: AgentSession, action: AgentAction, result: ToolResult) -> None: session.messages.append( { "role": "user", "content": "Tool observation:\n" + json.dumps( { "action": asdict(action), "result": asdict(result), "catalog_tools_already_run": sorted(session.catalog_tools_run), "instruction": "Do not repeat catalog discovery tools that already ran. Choose the next useful tool.", }, ensure_ascii=False, default=str, ), } ) def _append_validation_error(session: AgentSession, action: AgentAction, error: str) -> None: session.messages.append( { "role": "user", "content": "Rejected model action:\n" + json.dumps( { "action": asdict(action), "error": error, "catalog_tools_already_run": sorted(session.catalog_tools_run), "instruction": "Choose a different valid action. Do not repeat rejected actions.", }, ensure_ascii=False, default=str, ), } ) def _record_event(session: AgentSession, event: AgentEvent, on_event: Callable[[AgentEvent], None] | None = None) -> None: session.events.append(event) if on_event: on_event(event) def _finish( session: AgentSession, status: AgentStatus, selected_package: dict[str, Any] | None = None, selected_resource: RetrievalResource | None = None, confidence: float = 0.0, clarification: str = "", ) -> AgentResult: result = AgentResult( status=status, endpoint=session.endpoint, prompt=session.prompt, messages=session.messages, events=session.events, packages=list(session.packages.values()), resources=list(session.resources.values()), selected_package=selected_package, selected_resource=selected_resource, confidence=confidence, clarification=clarification, ) session.events.append(AgentEvent("done", f"Agent stopped with status {status}.", {"status": status})) return result def _resources_from_package(package: dict[str, Any]) -> list[RetrievalResource]: package_id = _package_id(package) package_title = str(package.get("title") or package.get("name") or package_id or "CKAN package") resources: list[RetrievalResource] = [] for resource in package.get("resources") or []: if not isinstance(resource, dict): continue resource_id = str(resource.get("id") or "") if not resource_id: continue resources.append( RetrievalResource( package_id=package_id, package_title=package_title, resource_id=resource_id, name=str(resource.get("name") or resource.get("description") or resource.get("url") or "resource"), format=str(resource.get("format") or resource.get("mimetype") or "").lower(), url=str(resource.get("url") or ""), ) ) return resources def _best_observed_resource(session: AgentSession) -> RetrievalResource | None: best: RetrievalResource | None = None best_score = 0.0 for resource in session.resources.values(): score = _score_resource(resource, session.prompt) if score > best_score: best = resource best_score = score return best def _score_resource(resource: RetrievalResource, prompt: str) -> float: haystack = f"{resource.package_title} {resource.name} {resource.format} {resource.url}".casefold() terms = [term for term in re.findall(r"[\w-]+", prompt.casefold()) if len(term) > 2] score = 0.15 if "csv" in resource.format or resource.url.casefold().endswith(".csv"): score += 0.45 if any(term in haystack for term in terms): score += 0.25 if any(term in haystack for term in ("fahrrad", "rad", "raddauer", "radverkehr")) and any(term in prompt.casefold() for term in ("fahrrad", "fahrräder", "rad", "bike", "bicycle")): score += 0.25 if "metadata" in haystack or "metadaten" in haystack: score -= 0.2 return max(0.0, min(score, 1.0)) def _compact_package(package: dict[str, Any]) -> dict[str, Any]: return { "package_id": _package_id(package), "name": str(package.get("name") or ""), "title": str(package.get("title") or package.get("name") or ""), "resources": len(package.get("resources") or []), } def _package_id(package: dict[str, Any]) -> str: return str(package.get("id") or package.get("name") or "") def _fallback_query( prompt: str, searched_queries: list[str] | int | None = None, discovered_tags: list[str] | None = None, discovered_groups: list[dict[str, Any]] | None = None, ) -> str: if isinstance(searched_queries, int): searched = [] index = searched_queries else: searched = list(searched_queries or []) index = len(searched) base = _catalog_query(prompt) discovered = _matching_discovered_queries(discovered_tags or [], discovered_groups or []) variants = _unique_queries([*discovered, base, f"{base} csv"]) for query in variants: if query not in searched: return query return variants[min(index, len(variants) - 1)] def _matching_discovered_queries(tags: list[str], groups: list[dict[str, Any]]) -> list[str]: candidates: list[str] = [] for tag in tags: clean = str(tag).strip() if clean: candidates.append(clean) for group in groups: label = str(group.get("title") or group.get("display_name") or group.get("name") or "").strip() if label: candidates.append(label) return _unique_queries(candidates) def _catalog_query(prompt: str) -> str: return _clean_prompt(prompt) or "open data" def _search_terms(prompt: str) -> list[str]: prompt = _clean_prompt(prompt) terms = [] for term in re.findall(r"[\w-]+", prompt.casefold()): clean = term.strip("-_") if len(clean) <= 2 or clean.isdigit(): continue if clean not in terms: terms.append(clean) return terms def _unique_queries(queries: list[str]) -> list[str]: unique = [] for query in queries: clean = " ".join(query.split()).strip() if clean and clean not in unique: unique.append(clean) return unique or ["open data"] def _clean_prompt(prompt: str) -> str: value = str(prompt or "").strip() content_match = re.search(r"(.*?)", value, re.IGNORECASE | re.DOTALL) if content_match: value = content_match.group(1).strip() value = re.sub(r".*?", " ", value, flags=re.IGNORECASE | re.DOTALL) value = re.sub(r"<[^>]+>", " ", value) value = re.sub(r"\s+", " ", value).strip() return value def _names_from_ckan_list_result(result: dict[str, Any]) -> list[str]: values = result.get("results") or result.get("value") or [] if not isinstance(values, list): return [] names: list[str] = [] for item in values: if isinstance(item, dict): name = str(item.get("name") or item.get("display_name") or "").strip() else: name = str(item).strip() if name and name not in names: names.append(name) return names def _dicts_from_ckan_list_result(result: dict[str, Any]) -> list[dict[str, Any]]: values = result.get("results") or result.get("value") or [] if not isinstance(values, list): return [] return [item for item in values if isinstance(item, dict)] def _compact_named_item(item: dict[str, Any]) -> dict[str, str]: return { "name": str(item.get("name") or ""), "title": str(item.get("title") or item.get("display_name") or item.get("name") or ""), } def _normalize_endpoint_or_default(endpoint: str | None) -> str: if not endpoint: return DEFAULT_CKAN_ENDPOINT try: return normalize_ckan_base_url(endpoint) except ValueError: return DEFAULT_CKAN_ENDPOINT def _short_error(exc: Exception) -> str: return f"{type(exc).__name__}: {str(exc).strip() or type(exc).__name__}" def _is_service_unavailable_error(error: str) -> bool: return "503" in error or "service unavailable" in error.casefold() def _should_stop_for_tool_failure(session: AgentSession, result: ToolResult) -> bool: if result.ok: return False if session.consecutive_service_errors >= 2: return True return any(count >= 2 for count in session.failed_searches.values())