|
85 | 85 | "outputs": [], |
86 | 86 | "source": [ |
87 | 87 | "exact_cases = data.load_exact_cases()\n", |
| 88 | + "\n", |
| 89 | + "# RAG step — retrieve. For each question, pull the most relevant docs with our\n", |
| 90 | + "# real ChromaDB retriever and attach them as `context`. A production RAG system\n", |
| 91 | + "# retrieves per query at answer time; we do it once here so every optimizer trial\n", |
| 92 | + "# answers the SAME question from the SAME context. What we optimize is the\n", |
| 93 | + "# *prompt*, not the retriever.\n", |
| 94 | + "for case in exact_cases:\n", |
| 95 | + " case[\"context\"] = \"\\n\\n\".join(rag_app.retrieve(case[\"query\"]))\n", |
| 96 | + "\n", |
88 | 97 | "exact_dataset = data.build_dataset(client, \"ledgerline-exact\", exact_cases)\n", |
89 | | - "print(f\"Dataset 'ledgerline-exact' has {len(exact_cases)} cases\")" |
| 98 | + "print(f\"Dataset 'ledgerline-exact' has {len(exact_cases)} cases (each with retrieved context)\")" |
90 | 99 | ] |
91 | 100 | }, |
92 | 101 | { |
|
128 | 137 | "source": [ |
129 | 138 | "#### The starting prompt\n", |
130 | 139 | "\n", |
131 | | - "Here is our baseline system prompt — deliberately mediocre, so there's room to improve. This is the `ChatPrompt` the optimizer will rewrite. `{query}` is filled from each dataset row." |
| 140 | + "Here is our baseline system prompt — deliberately mediocre, so there's room to improve. This is the `ChatPrompt` the optimizer will rewrite.\n", |
| 141 | + "\n", |
| 142 | + "Look at the **user template**: `{context}` is filled with the docs we just retrieved and `{query}` with the question — both come from each dataset row. That's the **retrieve-then-generate** shape of a real RAG system. Retrieval is held fixed; what we optimize is how the **system prompt** tells the model to turn that context into a correct, concise answer." |
132 | 143 | ] |
133 | 144 | }, |
134 | 145 | { |
|
145 | 156 | "prompt = ChatPrompt(\n", |
146 | 157 | " name=\"ledgerline-answer\",\n", |
147 | 158 | " system=BASELINE_SYSTEM,\n", |
148 | | - " user=\"{query}\",\n", |
| 159 | + " user=\"Context:\\n{context}\\n\\nQuestion: {query}\",\n", |
149 | 160 | " model=config.GEN_MODEL,\n", |
150 | 161 | ")" |
151 | 162 | ] |
|
188 | 199 | "\n", |
189 | 200 | "print(\"Baseline score:\", result.initial_score)\n", |
190 | 201 | "print(\"Best score: \", result.score)\n", |
191 | | - "print(\"\\nOptimized system prompt:\\n\", result.prompt)" |
| 202 | + "\n", |
| 203 | + "# See HOW the prompt was refined: the optimizer rewrote the *system* instructions.\n", |
| 204 | + "# result.prompt is a ChatPrompt; .system is the optimized system text.\n", |
| 205 | + "print(\"\\n--- Baseline system prompt ---\")\n", |
| 206 | + "print(BASELINE_SYSTEM)\n", |
| 207 | + "print(\"\\n--- Optimized system prompt ---\")\n", |
| 208 | + "print(result.prompt.system)" |
192 | 209 | ] |
193 | 210 | }, |
194 | 211 | { |
|
198 | 215 | "source": [ |
199 | 216 | "#### See it in Opik\n", |
200 | 217 | "\n", |
201 | | - "Open **Evaluation → Optimization runs** in your Opik workspace. You'll see this run with every candidate prompt, its score, and the trace for each trial. Compare the baseline row to the best row — that delta is your improvement.\n", |
| 218 | + "The cell above printed the **baseline vs optimized system prompt** side by side — that rewrite is the concrete refinement the optimizer found. Now open **Evaluation → Optimization runs** in your Opik workspace: you'll see this run with every candidate prompt, its score, and the trace for each trial. Compare the baseline row to the best row — that delta is your improvement.\n", |
202 | 219 | "\n", |
203 | 220 | "🎓 **This is where the live workshop ends.** You've run a real optimization and improved a prompt, measured against a dataset, stored in Opik. Everything below builds on exactly this loop." |
204 | 221 | ] |
|
289 | 306 | "outputs": [], |
290 | 307 | "source": [ |
291 | 308 | "judge_cases = data.load_judge_cases()\n", |
| 309 | + "for case in judge_cases:\n", |
| 310 | + " case[\"context\"] = \"\\n\\n\".join(rag_app.retrieve(case[\"query\"]))\n", |
292 | 311 | "judge_dataset = data.build_dataset(client, \"ledgerline-judge\", judge_cases)\n", |
293 | 312 | "\n", |
294 | 313 | "judge_prompt = ChatPrompt(\n", |
295 | 314 | " name=\"ledgerline-answer-judge\",\n", |
296 | 315 | " system=BASELINE_SYSTEM,\n", |
297 | | - " user=\"{query}\",\n", |
| 316 | + " user=\"Context:\\n{context}\\n\\nQuestion: {query}\",\n", |
298 | 317 | " model=config.GEN_MODEL,\n", |
299 | 318 | ")\n", |
300 | 319 | "\n", |
|
352 | 371 | " n_samples=8,\n", |
353 | 372 | ")\n", |
354 | 373 | "print(\"Multi-objective best score:\", multi_result.score)\n", |
355 | | - "print(\"\\nOptimized prompt:\\n\", multi_result.prompt)" |
| 374 | + "print(\"\\nOptimized system prompt:\\n\", multi_result.prompt.system)" |
356 | 375 | ] |
357 | 376 | }, |
358 | 377 | { |
|
372 | 391 | "source": [ |
373 | 392 | "## Part 3 — From prompt to agent\n", |
374 | 393 | "\n", |
375 | | - "So far we optimized a single answer prompt. Real systems are agents: they *decide* what to do. Our RAG app can grow a **retrieval gate** — decide whether a question even needs a docs lookup (cheap questions skip retrieval). That decision is itself a prompt, and the same optimizer loop tunes it." |
| 394 | + "So far retrieval was **fixed**: we retrieved once, put the docs in the prompt, and optimized the wording. Real systems are agents — they *decide* what to do. Here we hand the model a **`search_docs` tool** wired to our retriever and let it choose when to call it. That turns the prompt into an **agent**, and the same optimizer loop tunes it.\n", |
| 395 | + "\n", |
| 396 | + "**What \"optimizing an agent\" means:** not rewriting the tool's code — the retriever is fixed. It means optimizing the natural-language surface the agent reasons over: its **system prompt** (when to search, how to answer from results) and, optionally, its **tool descriptions** (`optimize_tools=True`) so it calls the tool at the right moments.\n", |
| 397 | + "\n", |
| 398 | + "*(A lighter alternative to a tool is a yes/no retrieval gate — see `rag_app.should_retrieve` — optimized with the same loop. We use a real tool here because it better reflects a production agent.)*" |
376 | 399 | ] |
377 | 400 | }, |
378 | 401 | { |
|
382 | 405 | "metadata": {}, |
383 | 406 | "outputs": [], |
384 | 407 | "source": [ |
385 | | - "@opik.track\n", |
386 | | - "def agentic_answer(query: str, system_prompt: str) -> str:\n", |
387 | | - " # Agent step: decide whether to retrieve, then answer accordingly.\n", |
388 | | - " if rag_app.should_retrieve(query):\n", |
389 | | - " return rag_app.answer(query, system_prompt=system_prompt)\n", |
390 | | - " messages = [\n", |
391 | | - " {\"role\": \"system\", \"content\": system_prompt},\n", |
392 | | - " {\"role\": \"user\", \"content\": query},\n", |
393 | | - " ]\n", |
394 | | - " import litellm\n", |
395 | | - " resp = litellm.completion(model=config.GEN_MODEL, messages=messages)\n", |
396 | | - " return resp.choices[0].message.content" |
| 408 | + "# The tool the agent may call. It wraps our real ChromaDB retriever; the *agent*\n", |
| 409 | + "# decides when to call it. Returning one string keeps the tool result clean.\n", |
| 410 | + "def search_docs(query: str) -> str:\n", |
| 411 | + " \"\"\"Search the Ledgerline documentation and return the most relevant snippets.\"\"\"\n", |
| 412 | + " return \"\\n\\n\".join(rag_app.retrieve(query))\n", |
| 413 | + "\n", |
| 414 | + "\n", |
| 415 | + "SEARCH_DOCS_TOOL = {\n", |
| 416 | + " \"type\": \"function\",\n", |
| 417 | + " \"function\": {\n", |
| 418 | + " \"name\": \"search_docs\",\n", |
| 419 | + " \"description\": \"Search the Ledgerline product documentation for relevant snippets.\",\n", |
| 420 | + " \"parameters\": {\n", |
| 421 | + " \"type\": \"object\",\n", |
| 422 | + " \"properties\": {\n", |
| 423 | + " \"query\": {\"type\": \"string\", \"description\": \"What to look up in the docs.\"},\n", |
| 424 | + " },\n", |
| 425 | + " \"required\": [\"query\"],\n", |
| 426 | + " },\n", |
| 427 | + " },\n", |
| 428 | + "}\n", |
| 429 | + "\n", |
| 430 | + "AGENT_SYSTEM = \"You are a Ledgerline support agent. Use tools when they help.\"\n", |
| 431 | + "\n", |
| 432 | + "# tools + function_map make this ChatPrompt an agent: on a tool call, the\n", |
| 433 | + "# optimizer executes search_docs and feeds the result back to the model.\n", |
| 434 | + "agent_prompt = ChatPrompt(\n", |
| 435 | + " name=\"ledgerline-agent\",\n", |
| 436 | + " system=AGENT_SYSTEM,\n", |
| 437 | + " user=\"{query}\",\n", |
| 438 | + " tools=[SEARCH_DOCS_TOOL],\n", |
| 439 | + " function_map={\"search_docs\": search_docs},\n", |
| 440 | + " model=config.GEN_MODEL,\n", |
| 441 | + ")\n", |
| 442 | + "\n", |
| 443 | + "# optimize_prompts defaults to \"system\": we tune the agent's instructions.\n", |
| 444 | + "# (Flip optimize_tools=True to ALSO let the optimizer refine the tool description.)\n", |
| 445 | + "agent_result = optimizer.optimize_prompt(\n", |
| 446 | + " prompt=agent_prompt,\n", |
| 447 | + " dataset=judge_dataset,\n", |
| 448 | + " metric=answer_relevance,\n", |
| 449 | + " max_trials=8,\n", |
| 450 | + " n_samples=8,\n", |
| 451 | + " allow_tool_use=True,\n", |
| 452 | + ")\n", |
| 453 | + "print(\"Agent baseline:\", agent_result.initial_score, \"-> best:\", agent_result.score)\n", |
| 454 | + "print(\"\\n--- Optimized agent system prompt ---\")\n", |
| 455 | + "print(agent_result.prompt.system)" |
397 | 456 | ] |
398 | 457 | }, |
399 | 458 | { |
|
403 | 462 | "source": [ |
404 | 463 | "#### When demonstrations matter: Few-Shot Bayesian\n", |
405 | 464 | "\n", |
406 | | - "If the win comes from *showing examples* rather than rewording instructions, reach for `FewShotBayesianOptimizer` — it uses Bayesian search (Optuna) to pick the best set and order of few-shot demonstrations to attach to your prompt. Same API as before." |
| 465 | + "If the win comes from *showing examples* rather than rewording instructions, reach for `FewShotBayesianOptimizer` — it uses Bayesian search (Optuna) to pick the best set and order of few-shot demonstrations to attach. We point it at the **same agent**, so it tunes the agent's examples rather than a fresh prompt." |
407 | 466 | ] |
408 | 467 | }, |
409 | 468 | { |
|
418 | 477 | "fewshot_optimizer = FewShotBayesianOptimizer(model=config.OPTIMIZER_MODEL, n_threads=4)\n", |
419 | 478 | "\n", |
420 | 479 | "fewshot_result = fewshot_optimizer.optimize_prompt(\n", |
421 | | - " prompt=judge_prompt,\n", |
| 480 | + " prompt=agent_prompt,\n", |
422 | 481 | " dataset=judge_dataset,\n", |
423 | 482 | " metric=answer_relevance,\n", |
424 | 483 | " n_samples=8,\n", |
|
452 | 511 | "| **Few-Shot Bayesian** | Picking the best demonstrations | Part 3 |\n", |
453 | 512 | "| **Evolutionary** | Exploring diverse structures; multi-objective | (see multi-objective, Part 2) |\n", |
454 | 513 | "| **GEPA** | Single-turn, reflection-heavy tasks (`pip install gepa`) | (try on your own) |\n", |
455 | | - "| **Parameter** | Temperature / top_p, prompt unchanged | Part 3 |\n", |
| 514 | + "| **Parameter** | Temperature / top_p, prompt unchanged | Part 3 (described) |\n", |
456 | 515 | "\n", |
457 | 516 | "**How to choose, in four questions:**\n", |
458 | 517 | "1. **What's the constraint** — wording, examples, tool use, or sampling params?\n", |
|
0 commit comments