From 2e10c3d279eceade5374edc7d840b2f184c0973f Mon Sep 17 00:00:00 2001 From: sridhar Date: Thu, 20 Aug 2026 09:16:19 +0000 Subject: [PATCH 1/4] okf: add usage-based navigation hints --- okf/SPEC.md | 51 ++++- okf/examples/usage-hints/index.md | 8 + .../usage-hints/metrics/regional-revenue.md | 9 + okf/examples/usage-hints/metrics/revenue.md | 10 + okf/examples/usage-hints/usage.yaml | 18 ++ okf/src/reference_agent/bundle/__init__.py | 16 ++ okf/src/reference_agent/bundle/usage.py | 207 ++++++++++++++++++ okf/src/reference_agent/cli.py | 39 ++++ okf/tests/test_usage.py | 60 +++++ 9 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 okf/examples/usage-hints/index.md create mode 100644 okf/examples/usage-hints/metrics/regional-revenue.md create mode 100644 okf/examples/usage-hints/metrics/revenue.md create mode 100644 okf/examples/usage-hints/usage.yaml create mode 100644 okf/src/reference_agent/bundle/usage.py create mode 100644 okf/tests/test_usage.py diff --git a/okf/SPEC.md b/okf/SPEC.md index a516d501..d61fa7e4 100644 --- a/okf/SPEC.md +++ b/okf/SPEC.md @@ -116,6 +116,7 @@ sense for the knowledge being captured. path/to/bundle/ index.md # Optional. Directory listing for progressive disclosure. log.md # Optional. Chronological history of updates. + usage.yaml # Optional. Advisory usage/navigation hints. .md # A concept at the bundle root. / # Subdirectories organize concepts into groups. index.md @@ -133,13 +134,14 @@ A bundle MAY be distributed as: ### 3.1 Reserved filenames -The following filenames have defined meaning at any level of the -hierarchy and MUST NOT be used for concept documents: +The following filenames have defined meaning and MUST NOT be used for +concept documents: | Filename | Purpose | |------------|----------------------------------| | `index.md` | Directory listing. See §8. | | `log.md` | Update history. See §9. | +| `usage.yaml` | Bundle-root usage/navigation hints. See §9.1. | All other `.md` files are concept documents. @@ -515,9 +517,7 @@ sections, each grouping concepts under a heading: * [Title 1](relative-url-1) - short description of item 1 * [Title 2](relative-url-2) - short description of item 2 - # Another Section - * [Subdirectory](subdir/) - short description of the subdirectory ``` @@ -548,6 +548,45 @@ Date headings MUST use ISO 8601 `YYYY-MM-DD` form. Log entries are prose; the leading bold word (`**Update**`, `**Creation**`, `**Deprecation**`) is a convention, not a requirement. +### 9.1 Usage and navigation hints + +A bundle MAY contain a `usage.yaml` file at its root. It records aggregated, +derived observations about which concepts have historically been useful for +an intent and set of conditions. It is a navigation signal, not knowledge: +consumers MUST treat it as advisory and MUST read the referenced concept as +the source of truth. + +The initial schema is: + +```yaml +version: 1 +usage_hints: + - concept: metrics/revenue.md + intent: revenue_calculation + conditions: + period: monthly + aggregation: total + access_count: 514 + successful_count: 462 + last_accessed: 2026-08-20T08:30:00Z +``` + +`concept` is a bundle-relative path to a non-reserved Markdown concept. +`intent` and condition keys are producer-defined strings. `access_count` is +the number of observed accesses; `successful_count`, when present, counts +accesses that contributed to a successful resolution and MUST NOT exceed +`access_count`. `last_accessed` is an optional ISO 8601 timestamp. + +Usage data SHOULD be aggregated across consumers and MAY be updated in +batches. Producers MUST NOT store raw user queries or per-user history in +this file. Consumers MAY use recency, success rate, frequency, keyword +search, vector search, or graph traversal when ranking candidates; OKF does +not prescribe a matching algorithm. A usage hint MUST NOT be used to answer +a request without retrieving and evaluating the referenced concept. + +The file is optional. Consumers that do not support it MUST ignore it, and +consumers MUST tolerate unknown top-level and entry fields. + --- ## 10. Attested computations concept @@ -737,8 +776,8 @@ A bundle is **conformant** with OKF v0.2 if: 1. Every non-reserved `.md` file in the tree contains a parseable YAML frontmatter block. 2. Every frontmatter block contains a non-empty `type` field. -3. Every reserved filename (`index.md`, `log.md`) follows the structure in - §8 and §9 respectively when present. +3. Every reserved filename (`index.md`, `log.md`, `usage.yaml`) follows the + structure in §8, §9, and §9.1 respectively when present. When the trust, lifecycle, provenance, or computation families are present, producers SHOULD follow §5 through §10, and consumers: diff --git a/okf/examples/usage-hints/index.md b/okf/examples/usage-hints/index.md new file mode 100644 index 00000000..7a666018 --- /dev/null +++ b/okf/examples/usage-hints/index.md @@ -0,0 +1,8 @@ +# Usage Hints Example + +This bundle demonstrates optional advisory navigation metadata. The metric +documents remain the source of truth; [usage.yaml](usage.yaml) only suggests +where a consumer might look first. + +* [Revenue](metrics/revenue.md) +* [Regional Revenue](metrics/regional-revenue.md) diff --git a/okf/examples/usage-hints/metrics/regional-revenue.md b/okf/examples/usage-hints/metrics/regional-revenue.md new file mode 100644 index 00000000..c1193293 --- /dev/null +++ b/okf/examples/usage-hints/metrics/regional-revenue.md @@ -0,0 +1,9 @@ +--- +type: Metric +title: Regional Revenue +description: Recognized revenue grouped by region. +--- + +# Definition + +Recognized revenue is grouped by the order's region. diff --git a/okf/examples/usage-hints/metrics/revenue.md b/okf/examples/usage-hints/metrics/revenue.md new file mode 100644 index 00000000..40ef244e --- /dev/null +++ b/okf/examples/usage-hints/metrics/revenue.md @@ -0,0 +1,10 @@ +--- +type: Metric +title: Revenue +description: Recognized revenue for a requested period. +--- + +# Definition + +Recognized revenue is the total value of completed orders in the requested +period. diff --git a/okf/examples/usage-hints/usage.yaml b/okf/examples/usage-hints/usage.yaml new file mode 100644 index 00000000..3baa2f24 --- /dev/null +++ b/okf/examples/usage-hints/usage.yaml @@ -0,0 +1,18 @@ +version: 1 +usage_hints: + - concept: metrics/revenue.md + intent: revenue_calculation + conditions: + period: monthly + aggregation: total + access_count: 514 + successful_count: 462 + last_accessed: 2026-08-20T08:30:00Z + - concept: metrics/regional-revenue.md + intent: revenue_calculation + conditions: + group_by: region + aggregation: total + access_count: 287 + successful_count: 241 + last_accessed: 2026-08-19T16:42:11Z diff --git a/okf/src/reference_agent/bundle/__init__.py b/okf/src/reference_agent/bundle/__init__.py index 2763c169..1c805114 100644 --- a/okf/src/reference_agent/bundle/__init__.py +++ b/okf/src/reference_agent/bundle/__init__.py @@ -1,6 +1,15 @@ from reference_agent.bundle.document import OKFDocument, REQUIRED_FRONTMATTER_KEYS from reference_agent.bundle.index import regenerate_indexes from reference_agent.bundle.paths import concept_id_to_path, path_to_concept_id +from reference_agent.bundle.usage import ( + UsageError, + UsageFile, + UsageHint, + load_usage, + rank_usage, + record_usage, + save_usage, +) __all__ = [ "OKFDocument", @@ -8,4 +17,11 @@ "concept_id_to_path", "path_to_concept_id", "regenerate_indexes", + "UsageError", + "UsageFile", + "UsageHint", + "load_usage", + "rank_usage", + "record_usage", + "save_usage", ] diff --git a/okf/src/reference_agent/bundle/usage.py b/okf/src/reference_agent/bundle/usage.py new file mode 100644 index 00000000..a6b93fb0 --- /dev/null +++ b/okf/src/reference_agent/bundle/usage.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import os +import tempfile +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path, PurePosixPath +from typing import Any + +import yaml + +USAGE_FILENAME = "usage.yaml" +USAGE_VERSION = 1 + + +class UsageError(ValueError): + """Raised when usage metadata is malformed or unsafe.""" + + +@dataclass +class UsageHint: + concept: str + intent: str + conditions: dict[str, Any] = field(default_factory=dict) + access_count: int = 0 + successful_count: int | None = None + last_accessed: str | None = None + extensions: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_mapping(cls, value: Any) -> "UsageHint": + if not isinstance(value, dict): + raise UsageError("Each usage_hints entry must be a mapping") + concept = value.get("concept") + intent = value.get("intent") + conditions = value.get("conditions", {}) + if not isinstance(concept, str) or not concept: + raise UsageError("Usage hint concept must be a non-empty string") + if not isinstance(intent, str) or not intent: + raise UsageError("Usage hint intent must be a non-empty string") + if not isinstance(conditions, dict): + raise UsageError("Usage hint conditions must be a mapping") + access_count = _non_negative_int(value.get("access_count", 0), "access_count") + successful_raw = value.get("successful_count") + successful_count = ( + None + if successful_raw is None + else _non_negative_int(successful_raw, "successful_count") + ) + if successful_count is not None and successful_count > access_count: + raise UsageError("successful_count cannot exceed access_count") + last_accessed = value.get("last_accessed") + if last_accessed is not None and not isinstance(last_accessed, str): + raise UsageError("last_accessed must be an ISO-8601 string") + extensions = { + key: item + for key, item in value.items() + if key not in { + "concept", "intent", "conditions", "access_count", + "successful_count", "last_accessed", + } + } + return cls( + concept=concept, + intent=intent, + conditions=dict(conditions), + access_count=access_count, + successful_count=successful_count, + last_accessed=last_accessed, + extensions=extensions, + ) + + def to_mapping(self) -> dict[str, Any]: + value: dict[str, Any] = { + "concept": self.concept, + "intent": self.intent, + } + if self.conditions: + value["conditions"] = self.conditions + value["access_count"] = self.access_count + if self.successful_count is not None: + value["successful_count"] = self.successful_count + if self.last_accessed is not None: + value["last_accessed"] = self.last_accessed + value.update(self.extensions) + return value + + +@dataclass +class UsageFile: + hints: list[UsageHint] = field(default_factory=list) + version: int = USAGE_VERSION + extensions: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_mapping(cls, value: Any) -> "UsageFile": + if not isinstance(value, dict): + raise UsageError("usage.yaml must contain a mapping") + version = value.get("version", USAGE_VERSION) + if version != USAGE_VERSION: + raise UsageError(f"Unsupported usage.yaml version: {version!r}") + raw_hints = value.get("usage_hints", []) + if not isinstance(raw_hints, list): + raise UsageError("usage_hints must be a list") + hints = [UsageHint.from_mapping(item) for item in raw_hints] + extensions = { + key: item for key, item in value.items() if key not in {"version", "usage_hints"} + } + return cls(hints=hints, version=version, extensions=extensions) + + def to_mapping(self) -> dict[str, Any]: + value = { + "version": self.version, + "usage_hints": [hint.to_mapping() for hint in self.hints], + } + value.update(self.extensions) + return value + + +def _non_negative_int(value: Any, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise UsageError(f"{name} must be a non-negative integer") + return value + + +def _validate_concept_path(concept: str) -> None: + path = PurePosixPath(concept) + if path.is_absolute() or ".." in path.parts or path.suffix != ".md": + raise UsageError(f"Invalid usage hint concept path: {concept!r}") + if path.name in {"index.md", "log.md", "usage.yaml"}: + raise UsageError(f"Usage hint cannot target reserved file: {concept!r}") + + +def load_usage(bundle_root: Path) -> UsageFile: + path = bundle_root / USAGE_FILENAME + if not path.exists(): + return UsageFile() + try: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except yaml.YAMLError as error: + raise UsageError(f"Invalid YAML in {USAGE_FILENAME}: {error}") from error + usage = UsageFile.from_mapping(raw) + for hint in usage.hints: + _validate_concept_path(hint.concept) + return usage + + +def save_usage(bundle_root: Path, usage: UsageFile) -> Path: + bundle_root.mkdir(parents=True, exist_ok=True) + path = bundle_root / USAGE_FILENAME + payload = yaml.safe_dump(usage.to_mapping(), sort_keys=False) + fd, temporary = tempfile.mkstemp(prefix=".usage-", dir=bundle_root, text=True) + try: + with os.fdopen(fd, "w", encoding="utf-8") as stream: + stream.write(payload) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + return path + + +def record_usage( + bundle_root: Path, + concept: str, + intent: str, + conditions: dict[str, Any] | None = None, + *, + successful: bool = False, + accessed_at: datetime | None = None, +) -> UsageHint: + _validate_concept_path(concept) + if not intent: + raise UsageError("intent must be a non-empty string") + normalized_conditions = dict(conditions or {}) + usage = load_usage(bundle_root) + for hint in usage.hints: + if (hint.concept, hint.intent, hint.conditions) == ( + concept, + intent, + normalized_conditions, + ): + break + else: + hint = UsageHint(concept=concept, intent=intent, conditions=normalized_conditions) + usage.hints.append(hint) + hint.access_count += 1 + if successful: + hint.successful_count = (hint.successful_count or 0) + 1 + hint.last_accessed = (accessed_at or datetime.now(timezone.utc)).isoformat() + save_usage(bundle_root, usage) + return hint + + +def rank_usage( + bundle_root: Path, + intent: str, + conditions: dict[str, Any] | None = None, +) -> list[UsageHint]: + requested = dict(conditions or {}) + candidates = [hint for hint in load_usage(bundle_root).hints if hint.intent == intent] + + def score(hint: UsageHint) -> tuple[int, int, str]: + matches = sum(requested.get(key) == value for key, value in hint.conditions.items()) + return matches, hint.access_count, hint.concept + + return sorted(candidates, key=score, reverse=True) \ No newline at end of file diff --git a/okf/src/reference_agent/cli.py b/okf/src/reference_agent/cli.py index 5b75a973..077f0fe1 100644 --- a/okf/src/reference_agent/cli.py +++ b/okf/src/reference_agent/cli.py @@ -8,6 +8,7 @@ from reference_agent.agent import DEFAULT_MODEL from reference_agent.bundle.paths import parse_concept_id +from reference_agent.bundle.usage import rank_usage, record_usage from reference_agent.runner import ReferenceRunner from reference_agent.sources.bigquery import BigQuerySource @@ -158,9 +159,32 @@ def _parser() -> argparse.ArgumentParser: "--name", default=None, help="Display name for the bundle (default: bundle directory name).", ) + + usage = sub.add_parser("usage", help="Read and record advisory usage hints.") + usage_sub = usage.add_subparsers(dest="usage_command", required=True) + record = usage_sub.add_parser("record", help="Record one concept access.") + record.add_argument("--bundle", required=True, type=Path) + record.add_argument("--concept", required=True) + record.add_argument("--intent", required=True) + record.add_argument("--condition", action="append", default=[], metavar="KEY=VALUE") + record.add_argument("--successful", action="store_true") + rank = usage_sub.add_parser("rank", help="Rank hints for an intent.") + rank.add_argument("--bundle", required=True, type=Path) + rank.add_argument("--intent", required=True) + rank.add_argument("--condition", action="append", default=[], metavar="KEY=VALUE") return p +def _conditions(values: list[str]) -> dict[str, str]: + result: dict[str, str] = {} + for value in values: + key, separator, item = value.partition("=") + if not separator or not key: + raise SystemExit(f"Invalid --condition {value!r}; expected KEY=VALUE") + result[key] = item + return result + + def main(argv: list[str] | None = None) -> int: args = _parser().parse_args(argv) logging.basicConfig( @@ -212,4 +236,19 @@ def main(argv: list[str] | None = None) -> int: web_note = f"; web pass used {len(seeds)} seed(s)" if seeds else "; web pass skipped" print(f"Enriched {n} concept(s) into {args.out}{web_note}", file=sys.stderr) return 0 + if args.command == "usage": + conditions = _conditions(args.condition) + if args.usage_command == "record": + hint = record_usage( + args.bundle, + args.concept, + args.intent, + conditions, + successful=args.successful, + ) + print(f"Recorded {hint.concept} ({hint.access_count})") + return 0 + for hint in rank_usage(args.bundle, args.intent, conditions): + print(f"{hint.concept}\t{hint.access_count}") + return 0 return 1 diff --git a/okf/tests/test_usage.py b/okf/tests/test_usage.py new file mode 100644 index 00000000..cfd608a2 --- /dev/null +++ b/okf/tests/test_usage.py @@ -0,0 +1,60 @@ +from datetime import datetime, timezone + +import pytest + +from reference_agent.bundle.usage import UsageError, load_usage, rank_usage, record_usage + + +def test_missing_usage_file_is_backward_compatible(tmp_path): + assert load_usage(tmp_path).hints == [] + + +def test_record_merges_same_intent_and_conditions(tmp_path): + accessed_at = datetime(2026, 8, 20, 8, 30, tzinfo=timezone.utc) + record_usage(tmp_path, "metrics/revenue.md", "revenue", {"period": "monthly"}, accessed_at=accessed_at) + hint = record_usage( + tmp_path, + "metrics/revenue.md", + "revenue", + {"period": "monthly"}, + successful=True, + accessed_at=accessed_at, + ) + assert hint.access_count == 2 + assert hint.successful_count == 1 + assert hint.last_accessed == "2026-08-20T08:30:00+00:00" + assert len(load_usage(tmp_path).hints) == 1 + + +def test_rank_prefers_matching_conditions_then_frequency(tmp_path): + record_usage(tmp_path, "metrics/general.md", "revenue", {"period": "monthly"}) + record_usage(tmp_path, "metrics/regional.md", "revenue", {"period": "monthly", "group_by": "region"}) + record_usage(tmp_path, "metrics/regional.md", "revenue", {"period": "monthly", "group_by": "region"}) + ranked = rank_usage(tmp_path, "revenue", {"period": "monthly", "group_by": "region"}) + assert [hint.concept for hint in ranked] == ["metrics/regional.md", "metrics/general.md"] + + +@pytest.mark.parametrize("concept", ["../secret.md", "/tmp/secret.md", "index.md", "log.md"]) +def test_reserved_and_unsafe_concepts_are_rejected(tmp_path, concept): + with pytest.raises(UsageError): + record_usage(tmp_path, concept, "intent") + + +def test_invalid_usage_schema_is_rejected(tmp_path): + (tmp_path / "usage.yaml").write_text("version: 2\nusage_hints: []\n", encoding="utf-8") + with pytest.raises(UsageError, match="Unsupported"): + load_usage(tmp_path) + + +def test_unknown_fields_are_preserved(tmp_path): + (tmp_path / "usage.yaml").write_text( + "version: 1\nproducer: local\nusage_hints:\n" + " - concept: metrics/revenue.md\n" + " intent: revenue\n" + " access_count: 3\n" + " confidence: experimental\n", + encoding="utf-8", + ) + usage = load_usage(tmp_path) + assert usage.extensions == {"producer": "local"} + assert usage.hints[0].extensions == {"confidence": "experimental"} \ No newline at end of file From 6d960303c94dd363dfec661477cf904119997e45 Mon Sep 17 00:00:00 2001 From: sridhar Date: Thu, 20 Aug 2026 09:24:47 +0000 Subject: [PATCH 2/4] okf: rank usage hints with prompt similarity --- okf/pyproject.toml | 1 + okf/src/reference_agent/agent.py | 11 ++- okf/src/reference_agent/bundle/__init__.py | 2 + okf/src/reference_agent/bundle/usage.py | 78 ++++++++++++++++++- .../prompts/reference_instruction.md | 8 ++ .../prompts/web_ingestion_instruction.md | 6 +- okf/src/reference_agent/tools/bundle_tools.py | 56 +++++++++++++ okf/tests/test_usage.py | 38 ++++++++- okf/tests/test_usage_tools.py | 52 +++++++++++++ 9 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 okf/tests/test_usage_tools.py diff --git a/okf/pyproject.toml b/okf/pyproject.toml index 8686ec32..e54b05a5 100644 --- a/okf/pyproject.toml +++ b/okf/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "pyyaml>=6.0", "pydantic>=2.0", "markdownify>=0.11", + "rapidfuzz>=3.0", ] [project.optional-dependencies] diff --git a/okf/src/reference_agent/agent.py b/okf/src/reference_agent/agent.py index 1201ae0c..14c97d14 100644 --- a/okf/src/reference_agent/agent.py +++ b/okf/src/reference_agent/agent.py @@ -5,7 +5,12 @@ from google.adk import Agent from google.adk.tools import FunctionTool -from reference_agent.tools.bundle_tools import read_existing_doc, write_concept_doc +from reference_agent.tools.bundle_tools import ( + rank_concepts_for_intent, + read_existing_doc, + record_concept_usage, + write_concept_doc, +) from reference_agent.tools.source_tools import ( list_concepts, read_concept_raw, @@ -34,6 +39,8 @@ def build_bq_agent(model: str = DEFAULT_MODEL) -> Agent: FunctionTool(read_concept_raw), FunctionTool(sample_rows), FunctionTool(read_existing_doc), + FunctionTool(rank_concepts_for_intent), + FunctionTool(record_concept_usage), FunctionTool(write_concept_doc), ], ) @@ -48,6 +55,8 @@ def build_web_agent(model: str = DEFAULT_MODEL) -> Agent: FunctionTool(list_concepts), FunctionTool(read_concept_raw), FunctionTool(read_existing_doc), + FunctionTool(rank_concepts_for_intent), + FunctionTool(record_concept_usage), FunctionTool(write_concept_doc), FunctionTool(fetch_url), ], diff --git a/okf/src/reference_agent/bundle/__init__.py b/okf/src/reference_agent/bundle/__init__.py index 1c805114..3e117652 100644 --- a/okf/src/reference_agent/bundle/__init__.py +++ b/okf/src/reference_agent/bundle/__init__.py @@ -9,6 +9,7 @@ rank_usage, record_usage, save_usage, + score_usage_hint, ) __all__ = [ @@ -24,4 +25,5 @@ "rank_usage", "record_usage", "save_usage", + "score_usage_hint", ] diff --git a/okf/src/reference_agent/bundle/usage.py b/okf/src/reference_agent/bundle/usage.py index a6b93fb0..3f483670 100644 --- a/okf/src/reference_agent/bundle/usage.py +++ b/okf/src/reference_agent/bundle/usage.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import math import tempfile from dataclasses import dataclass, field from datetime import datetime, timezone @@ -8,11 +9,13 @@ from typing import Any import yaml +from rapidfuzz.fuzz import token_set_ratio USAGE_FILENAME = "usage.yaml" USAGE_VERSION = 1 + class UsageError(ValueError): """Raised when usage metadata is malformed or unsafe.""" @@ -192,16 +195,85 @@ def record_usage( return hint +def score_usage_hint( + hint: UsageHint, + *, + prompt: str = "", + intent: str = "", + conditions: dict[str, Any] | None = None, + max_access_count: int = 0, +) -> dict[str, float]: + """Return an explainable 0..1 score for one advisory usage hint. + + The score is deliberately heuristic. It ranks where to look first; it + never establishes that a concept is correct. Weights favor request + matching over historical popularity so stale or popular concepts cannot + dominate a materially better match. + """ + requested_conditions = dict(conditions or {}) + signature = " ".join( + [hint.intent, hint.concept] + + [f"{key} {value}" for key, value in hint.conditions.items()] + ) + prompt_match = token_set_ratio(prompt, signature) / 100 if prompt else 0.0 + intent_match = token_set_ratio(intent, hint.intent) / 100 if intent else 0.0 + compared = len(requested_conditions) + condition_match = ( + sum(hint.conditions.get(key) == value for key, value in requested_conditions.items()) + / compared + if compared + else 0.0 + ) + frequency = ( + math.log1p(hint.access_count) / math.log1p(max_access_count) + if max_access_count > 0 and hint.access_count > 0 + else 0.0 + ) + success_rate = ( + hint.successful_count / hint.access_count + if hint.successful_count is not None and hint.access_count > 0 + else 0.0 + ) + total = ( + 0.35 * prompt_match + + 0.25 * intent_match + + 0.20 * condition_match + + 0.10 * frequency + + 0.10 * success_rate + ) + return { + "score": round(total, 6), + "prompt_match": round(prompt_match, 6), + "intent_match": intent_match, + "condition_match": round(condition_match, 6), + "frequency": round(frequency, 6), + "success_rate": round(success_rate, 6), + } + + def rank_usage( bundle_root: Path, intent: str, conditions: dict[str, Any] | None = None, + *, + prompt: str = "", ) -> list[UsageHint]: requested = dict(conditions or {}) - candidates = [hint for hint in load_usage(bundle_root).hints if hint.intent == intent] + candidates = [ + hint + for hint in load_usage(bundle_root).hints + if not intent or hint.intent == intent + ] + max_access_count = max((hint.access_count for hint in candidates), default=0) def score(hint: UsageHint) -> tuple[int, int, str]: - matches = sum(requested.get(key) == value for key, value in hint.conditions.items()) - return matches, hint.access_count, hint.concept + scored = score_usage_hint( + hint, + prompt=prompt, + intent=intent, + conditions=requested, + max_access_count=max_access_count, + ) + return scored["score"], hint.access_count, hint.concept return sorted(candidates, key=score, reverse=True) \ No newline at end of file diff --git a/okf/src/reference_agent/prompts/reference_instruction.md b/okf/src/reference_agent/prompts/reference_instruction.md index 8adce17d..07eae53a 100644 --- a/okf/src/reference_agent/prompts/reference_instruction.md +++ b/okf/src/reference_agent/prompts/reference_instruction.md @@ -4,6 +4,10 @@ concept and finishes by calling `write_concept_doc` exactly once. ## Workflow +0. If selecting knowledge for a prompt rather than only enriching the named + concept, call `rank_concepts_for_intent` with the prompt and any extracted + intent or conditions. Treat results as advisory starting points and read + the actual concept before relying on it. 1. Call `read_existing_doc(concept_id)` to see whether a prior document exists. If it does, use it as a starting point and refine rather than rewrite. 2. Call `read_concept_raw(concept_id)` to get structured metadata (schema, @@ -18,6 +22,10 @@ concept and finishes by calling `write_concept_doc` exactly once. your reply — the only way to persist a concept is the `write_concept_doc` call. Do not call any tools after that. +For consumer workflows, call `record_concept_usage` after reading a concept. +Set `successful=true` only when it contributed to a successful resolution. +Never answer from a usage hint alone. + ## Frontmatter (YAML) Only `type` is strictly required; the rest are strongly recommended. diff --git a/okf/src/reference_agent/prompts/web_ingestion_instruction.md b/okf/src/reference_agent/prompts/web_ingestion_instruction.md index 30305dc0..84df6f6b 100644 --- a/okf/src/reference_agent/prompts/web_ingestion_instruction.md +++ b/okf/src/reference_agent/prompts/web_ingestion_instruction.md @@ -14,6 +14,10 @@ The user message contains: ## Workflow +0. If selecting knowledge for a prompt rather than only processing supplied + URLs, call `rank_concepts_for_intent` with the prompt and any extracted + intent or conditions. Treat results as advisory starting points and read + the actual concept before relying on it. 1. Call `list_concepts()` once at the start to learn what concepts the bundle already has. You will route web findings against these. 2. For each seed URL, call `fetch_url(url)`. The result includes the page's @@ -255,9 +259,7 @@ If a page surfaces several of these at once (a typical "data model" or "schema reference" page), make **multiple** `write_concept_doc` calls — one per affected concept — rather than dumping everything into one doc. - ## Style and integrity - - Record in `sources` **only** URLs you actually fetched (or URLs already present in the doc you're refining). Do not invent URLs. - Be concrete. Use concrete field names, concrete enum values, concrete diff --git a/okf/src/reference_agent/tools/bundle_tools.py b/okf/src/reference_agent/tools/bundle_tools.py index 34ad7d9d..efef9da8 100644 --- a/okf/src/reference_agent/tools/bundle_tools.py +++ b/okf/src/reference_agent/tools/bundle_tools.py @@ -10,6 +10,7 @@ OKFDocumentError, ) from reference_agent.bundle.paths import concept_id_to_path, parse_concept_id +from reference_agent.bundle.usage import load_usage, rank_usage, record_usage, score_usage_hint from reference_agent.tools.context import get_context, is_web_pass _PREFERRED_KEY_ORDER = ( @@ -87,6 +88,61 @@ def read_existing_doc(concept_id: str) -> dict[str, Any] | None: return {"frontmatter": doc.frontmatter, "body": doc.body} +def rank_concepts_for_intent( + prompt: str = "", + intent: str = "", + conditions: dict[str, Any] | None = None, + limit: int = 5, +) -> list[dict[str, Any]]: + """Return advisory cached candidates with explainable match scores.""" + if limit < 1: + raise ValueError("limit must be positive") + ctx = get_context() + usage = load_usage(ctx.bundle_root) + ranked = rank_usage(ctx.bundle_root, intent, conditions, prompt=prompt) + max_access_count = max((hint.access_count for hint in usage.hints), default=0) + return [ + { + "concept": hint.concept.removesuffix(".md"), + "path": hint.concept, + "score": score_usage_hint( + hint, + prompt=prompt, + intent=intent, + conditions=conditions, + max_access_count=max_access_count, + ), + "advisory": True, + } + for hint in ranked[:limit] + ] + + +def record_concept_usage( + concept_id: str, + intent: str, + conditions: dict[str, Any] | None = None, + successful: bool = False, +) -> dict[str, Any]: + """Record a concept access for future advisory navigation ranking.""" + ctx = get_context() + concept_path = concept_id_to_path(ctx.bundle_root, parse_concept_id(concept_id)) + hint = record_usage( + ctx.bundle_root, + concept_path.relative_to(ctx.bundle_root).as_posix(), + intent, + conditions, + successful=successful, + ) + return { + "concept": concept_id, + "intent": hint.intent, + "access_count": hint.access_count, + "successful_count": hint.successful_count, + "advisory": True, + } + + def write_concept_doc( concept_id: str, frontmatter: dict[str, Any], diff --git a/okf/tests/test_usage.py b/okf/tests/test_usage.py index cfd608a2..dff31f2f 100644 --- a/okf/tests/test_usage.py +++ b/okf/tests/test_usage.py @@ -2,7 +2,14 @@ import pytest -from reference_agent.bundle.usage import UsageError, load_usage, rank_usage, record_usage +from reference_agent.bundle.usage import ( + UsageError, + UsageHint, + load_usage, + rank_usage, + record_usage, + score_usage_hint, +) def test_missing_usage_file_is_backward_compatible(tmp_path): @@ -34,6 +41,35 @@ def test_rank_prefers_matching_conditions_then_frequency(tmp_path): assert [hint.concept for hint in ranked] == ["metrics/regional.md", "metrics/general.md"] +def test_score_explains_prompt_and_history_signals(): + hint = UsageHint( + concept="metrics/regional-revenue.md", + intent="revenue_calculation", + conditions={"group_by": "region"}, + access_count=8, + successful_count=6, + ) + score = score_usage_hint( + hint, + prompt="Show revenue by region", + intent="revenue_calculation", + conditions={"group_by": "region"}, + max_access_count=16, + ) + assert score["prompt_match"] > 0 + assert score["intent_match"] == 1 + assert score["condition_match"] == 1 + assert score["success_rate"] == 0.75 + assert 0 < score["score"] <= 1 + + +def test_rank_can_use_prompt_without_preclassified_intent(tmp_path): + record_usage(tmp_path, "metrics/revenue.md", "revenue_calculation", {"period": "monthly"}) + record_usage(tmp_path, "tables/orders.md", "order_lookup", {"entity": "order"}) + ranked = rank_usage(tmp_path, "", {"period": "monthly"}, prompt="monthly revenue") + assert ranked[0].concept == "metrics/revenue.md" + + @pytest.mark.parametrize("concept", ["../secret.md", "/tmp/secret.md", "index.md", "log.md"]) def test_reserved_and_unsafe_concepts_are_rejected(tmp_path, concept): with pytest.raises(UsageError): diff --git a/okf/tests/test_usage_tools.py b/okf/tests/test_usage_tools.py new file mode 100644 index 00000000..800eb2af --- /dev/null +++ b/okf/tests/test_usage_tools.py @@ -0,0 +1,52 @@ +from pathlib import Path +from unittest.mock import MagicMock + +from reference_agent.bundle.usage import record_usage +from reference_agent.tools.bundle_tools import ( + rank_concepts_for_intent, + record_concept_usage, +) +from reference_agent.tools.context import set_context + + +def _set_context(bundle_root: Path) -> None: + set_context(MagicMock(), bundle_root, model="test") + + +def test_rank_tool_returns_explainable_advisory_scores(tmp_path: Path): + record_usage( + tmp_path, + "metrics/revenue.md", + "revenue_calculation", + {"period": "monthly"}, + successful=True, + ) + _set_context(tmp_path) + + results = rank_concepts_for_intent( + prompt="Show monthly revenue", + conditions={"period": "monthly"}, + ) + + assert results[0]["concept"] == "metrics/revenue" + assert results[0]["advisory"] is True + assert results[0]["score"]["condition_match"] == 1 + + +def test_record_tool_uses_concept_ids(tmp_path: Path): + _set_context(tmp_path) + + result = record_concept_usage( + "metrics/revenue", + "revenue_calculation", + {"period": "monthly"}, + successful=True, + ) + + assert result == { + "concept": "metrics/revenue", + "intent": "revenue_calculation", + "access_count": 1, + "successful_count": 1, + "advisory": True, + } From eeb86eb6fbf8a5a1141090a22b176997d8835c61 Mon Sep 17 00:00:00 2001 From: sridhar Date: Thu, 20 Aug 2026 09:31:56 +0000 Subject: [PATCH 3/4] okf: keep usage hints implementation minimal --- okf/SPEC.md | 10 ++- okf/pyproject.toml | 1 - okf/src/reference_agent/agent.py | 11 +-- okf/src/reference_agent/bundle/__init__.py | 2 - okf/src/reference_agent/bundle/usage.py | 73 +------------------ .../prompts/reference_instruction.md | 8 -- .../prompts/web_ingestion_instruction.md | 4 - okf/src/reference_agent/tools/bundle_tools.py | 56 -------------- okf/tests/test_usage.py | 29 -------- okf/tests/test_usage_tools.py | 52 ------------- 10 files changed, 11 insertions(+), 235 deletions(-) delete mode 100644 okf/tests/test_usage_tools.py diff --git a/okf/SPEC.md b/okf/SPEC.md index d61fa7e4..c687597d 100644 --- a/okf/SPEC.md +++ b/okf/SPEC.md @@ -579,10 +579,12 @@ accesses that contributed to a successful resolution and MUST NOT exceed Usage data SHOULD be aggregated across consumers and MAY be updated in batches. Producers MUST NOT store raw user queries or per-user history in -this file. Consumers MAY use recency, success rate, frequency, keyword -search, vector search, or graph traversal when ranking candidates; OKF does -not prescribe a matching algorithm. A usage hint MUST NOT be used to answer -a request without retrieving and evaluating the referenced concept. +this file, and implementations MUST NOT require personally identifying +information in `usage.yaml`. Consumers MAY use recency, success rate, +frequency, keyword search, vector search, or graph traversal when ranking +candidates; OKF does not prescribe a matching algorithm. A usage hint MUST +NOT be used to answer a request without retrieving and evaluating the +referenced concept. The file is optional. Consumers that do not support it MUST ignore it, and consumers MUST tolerate unknown top-level and entry fields. diff --git a/okf/pyproject.toml b/okf/pyproject.toml index e54b05a5..8686ec32 100644 --- a/okf/pyproject.toml +++ b/okf/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "pyyaml>=6.0", "pydantic>=2.0", "markdownify>=0.11", - "rapidfuzz>=3.0", ] [project.optional-dependencies] diff --git a/okf/src/reference_agent/agent.py b/okf/src/reference_agent/agent.py index 14c97d14..1201ae0c 100644 --- a/okf/src/reference_agent/agent.py +++ b/okf/src/reference_agent/agent.py @@ -5,12 +5,7 @@ from google.adk import Agent from google.adk.tools import FunctionTool -from reference_agent.tools.bundle_tools import ( - rank_concepts_for_intent, - read_existing_doc, - record_concept_usage, - write_concept_doc, -) +from reference_agent.tools.bundle_tools import read_existing_doc, write_concept_doc from reference_agent.tools.source_tools import ( list_concepts, read_concept_raw, @@ -39,8 +34,6 @@ def build_bq_agent(model: str = DEFAULT_MODEL) -> Agent: FunctionTool(read_concept_raw), FunctionTool(sample_rows), FunctionTool(read_existing_doc), - FunctionTool(rank_concepts_for_intent), - FunctionTool(record_concept_usage), FunctionTool(write_concept_doc), ], ) @@ -55,8 +48,6 @@ def build_web_agent(model: str = DEFAULT_MODEL) -> Agent: FunctionTool(list_concepts), FunctionTool(read_concept_raw), FunctionTool(read_existing_doc), - FunctionTool(rank_concepts_for_intent), - FunctionTool(record_concept_usage), FunctionTool(write_concept_doc), FunctionTool(fetch_url), ], diff --git a/okf/src/reference_agent/bundle/__init__.py b/okf/src/reference_agent/bundle/__init__.py index 3e117652..1c805114 100644 --- a/okf/src/reference_agent/bundle/__init__.py +++ b/okf/src/reference_agent/bundle/__init__.py @@ -9,7 +9,6 @@ rank_usage, record_usage, save_usage, - score_usage_hint, ) __all__ = [ @@ -25,5 +24,4 @@ "rank_usage", "record_usage", "save_usage", - "score_usage_hint", ] diff --git a/okf/src/reference_agent/bundle/usage.py b/okf/src/reference_agent/bundle/usage.py index 3f483670..45b6a8d7 100644 --- a/okf/src/reference_agent/bundle/usage.py +++ b/okf/src/reference_agent/bundle/usage.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import math import tempfile from dataclasses import dataclass, field from datetime import datetime, timezone @@ -9,7 +8,6 @@ from typing import Any import yaml -from rapidfuzz.fuzz import token_set_ratio USAGE_FILENAME = "usage.yaml" USAGE_VERSION = 1 @@ -195,68 +193,10 @@ def record_usage( return hint -def score_usage_hint( - hint: UsageHint, - *, - prompt: str = "", - intent: str = "", - conditions: dict[str, Any] | None = None, - max_access_count: int = 0, -) -> dict[str, float]: - """Return an explainable 0..1 score for one advisory usage hint. - - The score is deliberately heuristic. It ranks where to look first; it - never establishes that a concept is correct. Weights favor request - matching over historical popularity so stale or popular concepts cannot - dominate a materially better match. - """ - requested_conditions = dict(conditions or {}) - signature = " ".join( - [hint.intent, hint.concept] - + [f"{key} {value}" for key, value in hint.conditions.items()] - ) - prompt_match = token_set_ratio(prompt, signature) / 100 if prompt else 0.0 - intent_match = token_set_ratio(intent, hint.intent) / 100 if intent else 0.0 - compared = len(requested_conditions) - condition_match = ( - sum(hint.conditions.get(key) == value for key, value in requested_conditions.items()) - / compared - if compared - else 0.0 - ) - frequency = ( - math.log1p(hint.access_count) / math.log1p(max_access_count) - if max_access_count > 0 and hint.access_count > 0 - else 0.0 - ) - success_rate = ( - hint.successful_count / hint.access_count - if hint.successful_count is not None and hint.access_count > 0 - else 0.0 - ) - total = ( - 0.35 * prompt_match - + 0.25 * intent_match - + 0.20 * condition_match - + 0.10 * frequency - + 0.10 * success_rate - ) - return { - "score": round(total, 6), - "prompt_match": round(prompt_match, 6), - "intent_match": intent_match, - "condition_match": round(condition_match, 6), - "frequency": round(frequency, 6), - "success_rate": round(success_rate, 6), - } - - def rank_usage( bundle_root: Path, intent: str, conditions: dict[str, Any] | None = None, - *, - prompt: str = "", ) -> list[UsageHint]: requested = dict(conditions or {}) candidates = [ @@ -264,16 +204,11 @@ def rank_usage( for hint in load_usage(bundle_root).hints if not intent or hint.intent == intent ] - max_access_count = max((hint.access_count for hint in candidates), default=0) - def score(hint: UsageHint) -> tuple[int, int, str]: - scored = score_usage_hint( - hint, - prompt=prompt, - intent=intent, - conditions=requested, - max_access_count=max_access_count, + matches = sum( + hint.conditions.get(key) == value + for key, value in requested.items() ) - return scored["score"], hint.access_count, hint.concept + return matches, hint.access_count, hint.concept return sorted(candidates, key=score, reverse=True) \ No newline at end of file diff --git a/okf/src/reference_agent/prompts/reference_instruction.md b/okf/src/reference_agent/prompts/reference_instruction.md index 07eae53a..8adce17d 100644 --- a/okf/src/reference_agent/prompts/reference_instruction.md +++ b/okf/src/reference_agent/prompts/reference_instruction.md @@ -4,10 +4,6 @@ concept and finishes by calling `write_concept_doc` exactly once. ## Workflow -0. If selecting knowledge for a prompt rather than only enriching the named - concept, call `rank_concepts_for_intent` with the prompt and any extracted - intent or conditions. Treat results as advisory starting points and read - the actual concept before relying on it. 1. Call `read_existing_doc(concept_id)` to see whether a prior document exists. If it does, use it as a starting point and refine rather than rewrite. 2. Call `read_concept_raw(concept_id)` to get structured metadata (schema, @@ -22,10 +18,6 @@ concept and finishes by calling `write_concept_doc` exactly once. your reply — the only way to persist a concept is the `write_concept_doc` call. Do not call any tools after that. -For consumer workflows, call `record_concept_usage` after reading a concept. -Set `successful=true` only when it contributed to a successful resolution. -Never answer from a usage hint alone. - ## Frontmatter (YAML) Only `type` is strictly required; the rest are strongly recommended. diff --git a/okf/src/reference_agent/prompts/web_ingestion_instruction.md b/okf/src/reference_agent/prompts/web_ingestion_instruction.md index 84df6f6b..1aa78df8 100644 --- a/okf/src/reference_agent/prompts/web_ingestion_instruction.md +++ b/okf/src/reference_agent/prompts/web_ingestion_instruction.md @@ -14,10 +14,6 @@ The user message contains: ## Workflow -0. If selecting knowledge for a prompt rather than only processing supplied - URLs, call `rank_concepts_for_intent` with the prompt and any extracted - intent or conditions. Treat results as advisory starting points and read - the actual concept before relying on it. 1. Call `list_concepts()` once at the start to learn what concepts the bundle already has. You will route web findings against these. 2. For each seed URL, call `fetch_url(url)`. The result includes the page's diff --git a/okf/src/reference_agent/tools/bundle_tools.py b/okf/src/reference_agent/tools/bundle_tools.py index efef9da8..34ad7d9d 100644 --- a/okf/src/reference_agent/tools/bundle_tools.py +++ b/okf/src/reference_agent/tools/bundle_tools.py @@ -10,7 +10,6 @@ OKFDocumentError, ) from reference_agent.bundle.paths import concept_id_to_path, parse_concept_id -from reference_agent.bundle.usage import load_usage, rank_usage, record_usage, score_usage_hint from reference_agent.tools.context import get_context, is_web_pass _PREFERRED_KEY_ORDER = ( @@ -88,61 +87,6 @@ def read_existing_doc(concept_id: str) -> dict[str, Any] | None: return {"frontmatter": doc.frontmatter, "body": doc.body} -def rank_concepts_for_intent( - prompt: str = "", - intent: str = "", - conditions: dict[str, Any] | None = None, - limit: int = 5, -) -> list[dict[str, Any]]: - """Return advisory cached candidates with explainable match scores.""" - if limit < 1: - raise ValueError("limit must be positive") - ctx = get_context() - usage = load_usage(ctx.bundle_root) - ranked = rank_usage(ctx.bundle_root, intent, conditions, prompt=prompt) - max_access_count = max((hint.access_count for hint in usage.hints), default=0) - return [ - { - "concept": hint.concept.removesuffix(".md"), - "path": hint.concept, - "score": score_usage_hint( - hint, - prompt=prompt, - intent=intent, - conditions=conditions, - max_access_count=max_access_count, - ), - "advisory": True, - } - for hint in ranked[:limit] - ] - - -def record_concept_usage( - concept_id: str, - intent: str, - conditions: dict[str, Any] | None = None, - successful: bool = False, -) -> dict[str, Any]: - """Record a concept access for future advisory navigation ranking.""" - ctx = get_context() - concept_path = concept_id_to_path(ctx.bundle_root, parse_concept_id(concept_id)) - hint = record_usage( - ctx.bundle_root, - concept_path.relative_to(ctx.bundle_root).as_posix(), - intent, - conditions, - successful=successful, - ) - return { - "concept": concept_id, - "intent": hint.intent, - "access_count": hint.access_count, - "successful_count": hint.successful_count, - "advisory": True, - } - - def write_concept_doc( concept_id: str, frontmatter: dict[str, Any], diff --git a/okf/tests/test_usage.py b/okf/tests/test_usage.py index dff31f2f..d91d3abf 100644 --- a/okf/tests/test_usage.py +++ b/okf/tests/test_usage.py @@ -4,11 +4,9 @@ from reference_agent.bundle.usage import ( UsageError, - UsageHint, load_usage, rank_usage, record_usage, - score_usage_hint, ) @@ -41,33 +39,6 @@ def test_rank_prefers_matching_conditions_then_frequency(tmp_path): assert [hint.concept for hint in ranked] == ["metrics/regional.md", "metrics/general.md"] -def test_score_explains_prompt_and_history_signals(): - hint = UsageHint( - concept="metrics/regional-revenue.md", - intent="revenue_calculation", - conditions={"group_by": "region"}, - access_count=8, - successful_count=6, - ) - score = score_usage_hint( - hint, - prompt="Show revenue by region", - intent="revenue_calculation", - conditions={"group_by": "region"}, - max_access_count=16, - ) - assert score["prompt_match"] > 0 - assert score["intent_match"] == 1 - assert score["condition_match"] == 1 - assert score["success_rate"] == 0.75 - assert 0 < score["score"] <= 1 - - -def test_rank_can_use_prompt_without_preclassified_intent(tmp_path): - record_usage(tmp_path, "metrics/revenue.md", "revenue_calculation", {"period": "monthly"}) - record_usage(tmp_path, "tables/orders.md", "order_lookup", {"entity": "order"}) - ranked = rank_usage(tmp_path, "", {"period": "monthly"}, prompt="monthly revenue") - assert ranked[0].concept == "metrics/revenue.md" @pytest.mark.parametrize("concept", ["../secret.md", "/tmp/secret.md", "index.md", "log.md"]) diff --git a/okf/tests/test_usage_tools.py b/okf/tests/test_usage_tools.py deleted file mode 100644 index 800eb2af..00000000 --- a/okf/tests/test_usage_tools.py +++ /dev/null @@ -1,52 +0,0 @@ -from pathlib import Path -from unittest.mock import MagicMock - -from reference_agent.bundle.usage import record_usage -from reference_agent.tools.bundle_tools import ( - rank_concepts_for_intent, - record_concept_usage, -) -from reference_agent.tools.context import set_context - - -def _set_context(bundle_root: Path) -> None: - set_context(MagicMock(), bundle_root, model="test") - - -def test_rank_tool_returns_explainable_advisory_scores(tmp_path: Path): - record_usage( - tmp_path, - "metrics/revenue.md", - "revenue_calculation", - {"period": "monthly"}, - successful=True, - ) - _set_context(tmp_path) - - results = rank_concepts_for_intent( - prompt="Show monthly revenue", - conditions={"period": "monthly"}, - ) - - assert results[0]["concept"] == "metrics/revenue" - assert results[0]["advisory"] is True - assert results[0]["score"]["condition_match"] == 1 - - -def test_record_tool_uses_concept_ids(tmp_path: Path): - _set_context(tmp_path) - - result = record_concept_usage( - "metrics/revenue", - "revenue_calculation", - {"period": "monthly"}, - successful=True, - ) - - assert result == { - "concept": "metrics/revenue", - "intent": "revenue_calculation", - "access_count": 1, - "successful_count": 1, - "advisory": True, - } From 775741767570f5e362a85add34bd3588da1270b0 Mon Sep 17 00:00:00 2001 From: sridhar Date: Thu, 20 Aug 2026 09:37:35 +0000 Subject: [PATCH 4/4] small check --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e9f8707..cd903189 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ All solutions within this repository are provided under the [Apache 2.0](https:/ ## Disclaimer -This repository and its contents are not an official Google product. +This repository and its contents are not an official Google product. \ No newline at end of file