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:
+96
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user