Phase 11 + Phase 6 GPU move

## Phase 11 — Curated agronomy / label-handling knowledge layer

docs_mcp/lessons.md: 13 topic-anchored markdown sections covering
the LLM-side context a farmer-advisor needs alongside the raw
label corpus —
  - how-to-use-this-corpus
  - epa-signal-words
  - rei-phi-fundamentals
  - rup-handling
  - supplemental-labels-24c-2ee
  - tank-mix-fundamentals
  - resistance-management-hrac-frac-irac
  - glufosinate-application-rules
  - dicamba-application-rules
  - lake-erie-watershed-ohio
  - scn-and-other-seed-treatment-context
  - drift-management-essentials
  - how-to-format-recommendations

Each Topic block is independently retrievable via the new MCP tool:

  ppls_api_lessons(topic="rup-handling")

Or with no topic to get the full TOC, or with a substring to
match-and-return matching sections ("dicamba" → dicamba-application-rules).

Tool docstring instructs the LLM to call this proactively before any
pesticide recommendation so the recommendation lands with regulatory
framing, resistance-group callouts, RUP applicator language, and the
canonical recommendation format — not just a rate from a label.

## Phase 6 — Reranker moved to GPU on trashpanda

Stopped the local CPU container and started on trashpanda's Tesla P4
(8 GB VRAM) via:

  docker run -d --name llama-rerank --restart unless-stopped --gpus all \
    -p 8082:8080 \
    ghcr.io/ggml-org/llama.cpp:server-cuda \
    -hf gpustack/jina-reranker-v2-base-multilingual-GGUF:Q8_0 \
    --reranking --host 0.0.0.0 --port 8080 -ngl 99

The :server-cuda image variant (not :server) is required for CUDA
backend; -ngl 99 offloads all layers to GPU.

Latency: 50-doc rerank dropped from ~23 s on CPU to ~0.7-1.5 s on
the Tesla P4 — production-grade interactive speeds.

deploy/rerank-docker.md updated with the trashpanda deploy recipe,
troubleshooting (mostly "did you use server-cuda?"), and a perf
reference table. The MCP server's RERANK_URL just points at
http://10.10.1.65:8082 now.

GPU eval still completing in background; results land in
eval/results/with_rerank_gpu.md as a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 12:10:09 -04:00
parent 278fe5f456
commit af44d7a102
3 changed files with 410 additions and 27 deletions
+96 -5
View File
@@ -599,16 +599,107 @@ def corpus_status() -> str:
# ---------------------------------------------------------------------------
# Stubs for later phases — keep the signatures in this file so refactors
# don't lose the contracts. Implementations come per phase.
# Phase 11 — Curated agronomy / label-handling knowledge
# ---------------------------------------------------------------------------
LESSONS_PATH = Path(__file__).resolve().parent / "lessons.md"
_lessons_cache: tuple[str, list[tuple[str, str]]] | None = None # (full, sections)
def _load_lessons() -> tuple[str, list[tuple[str, str]]]:
"""Read lessons.md once, split into (topic_slug, body) sections."""
global _lessons_cache
if _lessons_cache is not None:
return _lessons_cache
if not LESSONS_PATH.exists():
_lessons_cache = ("", [])
return _lessons_cache
full = LESSONS_PATH.read_text(encoding="utf-8")
sections: list[tuple[str, str]] = []
# Split on lines like "## Topic: <slug>" (case-sensitive marker)
parts = re.split(r"^## Topic:\s+(\S+)\s*$", full, flags=re.MULTILINE)
# parts = [preamble, slug1, body1, slug2, body2, ...]
for i in range(1, len(parts), 2):
slug = parts[i].strip()
body = parts[i + 1].strip() if i + 1 < len(parts) else ""
sections.append((slug, body))
_lessons_cache = (full, sections)
return _lessons_cache
@mcp.tool()
def ppls_api_lessons(
topic: Annotated[
str | None,
Field(description="OPTIONAL: topic slug or substring (e.g., "
"'rup-handling', 'dicamba', 'rei'). Omit to get "
"the full table of contents."),
] = None,
) -> str:
"""Surface curated agronomy + label-handling knowledge that supplements
the raw label corpus.
**Call this proactively whenever you're about to give a pesticide
recommendation from search_docs results.** The lessons cover:
EPA signal words, REI/PHI fundamentals, RUP handling, 2(ee)/24(c)
supplemental labels, tank-mix and resistance-management
fundamentals (HRAC/FRAC/IRAC groups), product-specific application
rules (glufosinate, dicamba), Lake Erie watershed considerations
for Ohio, SCN context for soybean, drift management, and the
canonical recommendation format the farmer expects.
Without these lessons, your recommendations risk being technically
correct but missing the regulatory framing, resistance group
callouts, RUP applicator requirements, or off-target-damage
warnings that make them actionable. Call this first; cite specific
lessons in your response.
"""
with TimedCall("ppls_api_lessons", {"topic": topic}) as _call:
full, sections = _load_lessons()
if not sections:
_call.set(sections=0)
return "_(lessons.md not found — Phase 11 knowledge layer not populated)_"
if not topic:
_call.set(sections=len(sections), returned="toc")
toc_lines = [
"# PPLS API lessons — table of contents",
"",
f"Call `ppls_api_lessons(topic='<slug>')` to fetch a specific section.",
"",
]
for slug, body in sections:
# First non-blank, non-list line as the headline summary
summary = ""
for line in body.splitlines():
s = line.strip()
if s and not s.startswith(("-", "*", "|", "```")):
summary = s[:140]
break
toc_lines.append(f"- **`{slug}`** — {summary}")
return "\n".join(toc_lines)
topic_lc = topic.lower()
matched = [(slug, body) for slug, body in sections if topic_lc in slug.lower()]
if not matched:
_call.set(sections=0, returned="no-match")
available = ", ".join(f"`{s}`" for s, _ in sections)
return (f"_(no lesson matched topic `{topic}`. Available: {available})_")
_call.set(sections=len(matched), returned="match")
out: list[str] = []
for slug, body in matched:
out.append(f"## Topic: {slug}\n\n{body}")
return "\n\n---\n\n".join(out)
# ---------------------------------------------------------------------------
# Stubs for later phases
# ---------------------------------------------------------------------------
# @mcp.tool() # Phase 12
# def find_doc_inconsistencies(scope_query: str, ...) -> str: ...
# @mcp.tool() # Phase 11
# def ppls_label_lessons(topic: str | None = None) -> str: ...
# ===========================================================================
# Entry point