fix(agent): send the pipeline rules' STAGES to the model (CRM-235) - #49
Conversation
The lead says something, the agent should move the card, and nothing happens.
Not because the model refuses: the configured rules never reached the prompt.
The frontend saves PipelineRule { pipelineId, pipelineName, generalInstructions,
stages: StageRule[] } with StageRule { stageId, stageName, instructions }
(PipelineRules.tsx:17-32). The prompt builder read stageName/instructions off
the RULE, where they do not exist, so a two-stage funnel rendered as a single
line with the funnel name — no stage names, no stage ids, and none of the
per-stage "when to move" instructions the operator configured. The tool always
read rule["stages"] correctly; only the prompt side was wrong.
Since the same prompt says "do not move conversations between stages without a
matching rule", the model had nothing to match. Live check with Gemini
(gemini-3.5-flash), lead message "Fechado, pode gerar o pedido!":
before: {"stage_name": "Fechamento", "pipeline_id": "Vendas", ...}
^ stage hallucinated (real ones are Qualificado/Fechado), and the
funnel NAME sent where the id belongs
after: {"pipeline_id": "pipe-1", "stage_id": "stg-fechado", ...}
With the hallucinated args _move_to_stage looks the rule up by
pipelineId == "Vendas", finds nothing, and answers "stage_id or stage_name is
required. Available stages: none" — the card stays put and the operator sees
no reason, because the error dies inside the tool's return value.
Extracted _format_pipeline_rules_for_prompt: walks rule["stages"] and emits one
line per stage with name, id and its move-here-when instructions, plus the
funnel's generalInstructions. Legacy flat rules (stage on the rule itself)
still render, so configs saved by an older UI keep working.
Tests: 8 examples in tests/unit/test_pipeline_rules_prompt.py pinning the exact
shape the UI writes. Negative proof: restoring the old rule-level read makes 5
of them fail. Suite: 274 passed vs 266 on the clean develop baseline (+8, the
new ones); the 3 failures and 7 collection errors are identical to the baseline
and pre-existing (test_exception_handlers, mcp_headers_call_path).
Reviewer's GuideRefactors how pipeline rules are formatted into the LLM prompt so that stage-level data (IDs, names, and per-stage move instructions) is correctly included, adds support for legacy flat rule shapes, and introduces targeted unit tests to pin the new prompt contract and guard against regressions. Sequence diagram for pipeline rules reaching the LLM promptsequenceDiagram
participant Agent as LLM Agent Builder
participant Config as Agent Config
participant Formatter as _format_pipeline_rules_for_prompt
participant Model as LLM
participant Tool as Pipeline Manipulation Tool
Agent->>Config: get pipeline_rules
Config-->>Agent: PipelineRule with stages
Agent->>Formatter: _format_pipeline_rules_for_prompt(pipeline_rules)
Formatter-->>Agent: pipeline and stage prompt lines
Agent->>Model: create prompt with stage_id and move instructions
Model->>Tool: move_to_stage(pipeline_id, stage_id)
Tool-->>Model: stage movement result
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/services/adk/agents/llm_agent_builder.py" line_range="110-112" />
<code_context>
+ ``instructions`` off the rule yielded a prompt with the funnel name and
+ nothing else: no stage names, no stage ids, and none of the per-stage
+ "when to move" instructions the operator configured. Since the prompt also
+ tells the model "do not move conversations between stages without a
+ matching rule", an empty rule set means the model never moves the card —
+ and ``pipeline_manipulation`` answers "stage_id or stage_name is required"
+ when it is called blind. The tool itself always read ``rule["stages"]``
+ correctly; only this prompt side was wrong.
</code_context>
<issue_to_address>
**nitpick:** The new docstring states that an empty rule set means the model never moves the card, but `_create_llm_agent` takes a separate no-rules branch that explicitly tells the model to use the tool when warranted. The comment therefore misdescribes the actual behavior and can lead maintainers to incorrectly change the no-rules path.
**Triggers:** When an agent has no configured pipeline rules but still has pipeline manipulation enabled.
**Suggested fix:** Describe the old failure as the model lacking a matching configured rule, rather than claiming that an empty rule set categorically prevents moves.
</issue_to_address>Sourcery assessment
Needs a human reviewer. The new prompt can cause the model to move conversations into CRM pipeline stages based on the rendered rules, so an incorrect format or interpretation could leave conversations in wrong stages after the change is reverted. Those assignments are bounded and can be corrected or re-run, but reverting only prevents future moves and does not undo ones already made.
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Code review follow-ups on the same defect, at the edges the fix left open. A stage with no stageId was still rendered as "- Stage: unnamed stage — move here when: ...". The UI creates stages with stageId="" (PipelineRules.tsx) and AgentEditPage saves pipeline_rules verbatim, so an operator who writes the criteria and forgets to pick the stage in the select produces exactly that. The model then gets a rule that looks actionable with no id and no name, and invents a stage_name — the failure this card is about. Such stages are now skipped with a warning. The legacy flat branch dropped the stageId when the rule also carried a stageName, so it rendered the same id-less line the fix removes; and a flat rule cannot be recovered by name, because the tool resolves a name through rule["stages"], which a flat rule does not have. It now emits the id, and a flat rule without one is skipped for the same reason. When the formatter renders nothing (a pipeline_rules list holding no usable rule), the prompt used to say "Configured pipeline rules:" followed by an empty list and then "do not move without a matching rule" — advertising the tool and disabling it in the same breath. It now falls back to the generic text the no-rules branch already had. Comments trimmed to what the code does not say; the bug history lives in the PR and the card. Tests: 10 examples (8 kept, 2 added for the skipped shapes, 1 renamed to pin the id). Suite 294 passed vs 284 on the clean develop baseline (+10); the 2 failures and 1 collection error are identical to the baseline and pre-existing.
Problema
O lead fala na conversa, o agente deveria mover o card para o estágio configurado — e nada acontece. Não é o modelo se recusando: as regras de pipeline configuradas no agente nunca chegam ao prompt.
Causa-raiz
O frontend salva (
evo-ai-frontend-community→PipelineRules.tsx:17-32):Os estágios ficam dentro de
rule.stages.A tool sempre leu certo (
pipeline_manipulation.py:451→pipeline_rule.get("stages", [])). O builder do prompt (llm_agent_builder.py:763-773) liastageName/instructionsno nível da regra, onde não existem:Um funil com dois estágios renderizava assim (executado, não deduzido):
Sem nomes, sem
stageId, e sem as instruções de "quando mover" — que são exatamente o que o operador configura no modal.Por que isso trava a IA
O mesmo bloco de prompt termina com "do not move conversations between stages without a matching rule". O modelo é instruído a só agir com regra correspondente, e nenhuma chegou.
Comprovado ao vivo (Gemini
gemini-2.5-flash), lead dizendo "Fechado, pode gerar o pedido! Quero comprar o plano anual agora.":É pior do que não chamar: com esses argumentos,
_move_to_stageprocura a regra porpipelineId == "Vendas", não encontra (o id real épipe-1), fica comstagesvazio e responde"stage_id or stage_name is required. Available stages: none". O card não se move e o operador não vê motivo — o erro morre dentro do retorno da tool.Correção
Extraído
_format_pipeline_rules_for_prompt, que percorrerule["stages"]e emite uma linha por estágio, com nome, id e o critério de quando mover, mais asgeneralInstructionsdo funil:Formatos antigos (estágio no nível da regra) continuam renderizando, para não quebrar config salva por UI anterior.
Testes
tests/unit/test_pipeline_rules_prompt.py— 8 examples, fixando o formato exato que a UI escreve: cada estágio aparece, osstageIdsão expostos (sem eles a tool não tem como ser chamada), as instruções por estágio chegam, e o caso da regressão (funil renderizado como uma linha só).developlimpo — delta de exatamente+8(os novos). As 3 falhas e 7 erros de coleta são idênticos ao baseline e pré-existentes (test_exception_handlers,mcp_headers_call_path), sem relação com este PR.Escopo e relação com o CRM-213
Só
evo-ai-processor-community. A tool, o endpoint do CRM (PATCH /pipelines/:id/pipeline_items/:id/move_to_stage, que aceitaconversation_id) e a autorização (chamadas de serviço passam porservice_authenticated?) já estavam corretos.É o mesmo modal do CRM-213, mas defeito diferente: aquele fez as regras serem salvas; este faz com que, uma vez salvas, elas cheguem ao modelo. Por isso o CRM-213 sozinho não fez a IA mover o card.
Trade-off assumido
O E2E com o Gemini não entrou na suíte: depende de rede e da resposta do modelo, então falharia no CI por motivo alheio ao código. Rodei uma vez à mão e colei a saída acima e no card. O que protege contra a regressão no CI é o teste determinístico do formato.
Summary by Sourcery
Fix pipeline manipulation prompts so agents receive the configured pipeline stages and can move conversations using valid pipeline and stage identifiers.
Bug Fixes:
Enhancements:
Tests:
Ordem de merge — este é o primeiro de uma cadeia de 3
Os três PRs tocam o mesmo fluxo ("a IA não move o card") em camadas diferentes e foram
abertos encadeados, cada um com base no anterior:
#49 (CRM-235, este) → #50 (CRM-237) → #51 (CRM-238)
Este aqui tem base em
develope deve ser mergeado primeiro. Ao mergear, a base do #50precisa ser re-apontada para
develop(o GitHub costuma fazer isso sozinho quando a branchde origem é apagada — vale conferir).
Mergear fora de ordem traz os commits dos outros dois junto e o diff deixa de corresponder
ao que cada card descreve.