resolve.py 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. from __future__ import annotations
  2. import os
  3. import time
  4. import uuid
  5. from dataclasses import dataclass
  6. from typing import Any, Awaitable, Callable
  7. CallToolFn = Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]
  8. def _extract_bindings(result_payload: Any) -> list[dict[str, Any]]:
  9. """Best-effort extraction for Virtuoso/MCP-style SPARQL results."""
  10. if isinstance(result_payload, dict):
  11. return result_payload.get("results", {}).get("bindings", []) or []
  12. return []
  13. def _to_float(value: Any) -> float:
  14. try:
  15. return float(value)
  16. except Exception:
  17. return 0.0
  18. def _now_iso() -> str:
  19. # Avoid datetime imports; keep it lightweight.
  20. import datetime
  21. return datetime.datetime.now(datetime.timezone.utc).isoformat()
  22. def _build_candidates_query(*, subject: str, max_candidates: int, graph_iri: str) -> str:
  23. # Scaffolding query: adjust the predicate/shape once the remote schema is fixed.
  24. # Keep it deterministic and parameterized by the provided subject string.
  25. safe = subject.replace("\\", "\\\\").replace('"', '\\"')
  26. return f"""
  27. PREFIX atlas: <http://world.eu.org/atlas_ontology#>
  28. SELECT ?id ?label ?type ?source ?confidence ?description ?uri
  29. WHERE {{
  30. GRAPH <{graph_iri}> {{
  31. ?entity a atlas:Entity ;
  32. atlas:canonicalLabel ?label .
  33. FILTER(LCASE(STR(?label)) = LCASE(\"{safe}\"))
  34. OPTIONAL {{ ?entity atlas:hasCanonicalType ?type . }}
  35. OPTIONAL {{ ?entity atlas:canonicalDescription ?description . }}
  36. BIND(STR(?entity) AS ?uri)
  37. BIND(STRAFTER(STR(?entity), '#') AS ?id)
  38. BIND(0.9 AS ?confidence)
  39. BIND("sparql" AS ?source)
  40. }}
  41. }}
  42. LIMIT {max_candidates}
  43. """.strip()
  44. @dataclass
  45. class ResolveService:
  46. call_tool: CallToolFn | None = None
  47. async def _call_tool(self, tool_name: str, payload: dict[str, Any]) -> dict[str, Any]:
  48. if self.call_tool is None:
  49. # Important: default behavior is a stub. This scaffolding should run
  50. # safely without requiring a live remote MCP/Sparql backend.
  51. raise RuntimeError(
  52. "REMOTE_SPASQL_MCP_NOT_CONFIGURED (stub). "
  53. "Inject call_tool in tests or wire a real RemoteSparqlClient explicitly."
  54. )
  55. return await self.call_tool(tool_name, payload)
  56. async def resolve(
  57. self,
  58. *,
  59. subject: str,
  60. context: dict[str, Any] | None,
  61. constraints: dict[str, Any] | None,
  62. hints: dict[str, Any] | None,
  63. debug: dict[str, Any] | None,
  64. ) -> dict[str, Any]:
  65. # Stubbed implementation for “works first, decide logic later”.
  66. # We intentionally ignore inputs until you confirm the app structure.
  67. return {"status": "ok"}