From c6bf0f31ccd1ef2a2330f4e20be1482d52132146 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 20 May 2026 10:00:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(prompt=5Flibrary):=20v3.2=20custom=5Findic?= =?UTF-8?q?ators=5Fspec=20=E2=80=94=20fix=20LLM=20parse=20rate=20on=20Pyth?= =?UTF-8?q?agoras=20indicators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multi_swarm_core/agents/hypothesis.py | 6 +++ .../multi_swarm_core/genome/prompt_library.py | 13 ++++++ .../tests/unit/test_hypothesis_agent.py | 45 +++++++++++++++++++ .../tests/unit/test_prompt_library.py | 22 +++++++++ .../strategy_pythagoras/prompts.json | 4 +- 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py index aab649f..a6ae3a0 100644 --- a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py +++ b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py @@ -181,6 +181,12 @@ def _build_system_prompt(lib: PromptLibrary, genome: HypothesisAgentGenome) -> s parts.append("") # 3. Grammar spec (core scaffold) parts.append(_SYSTEM_GRAMMAR_SPEC) + # 3b. Custom indicators spec (da prompts.json, opzionale) - estende la lista + # "Leaf - indicatori" con firme strategy-specific non note al core. + if lib.custom_indicators_spec: + parts.append("\nINDICATORI CUSTOM (firme aggiuntive, applica le stesse regole grammaticali):\n") + parts.append(lib.custom_indicators_spec) + parts.append("") # 4. Pattern guidance (da prompts.json, opzionale) if lib.pattern_guidance: parts.append( diff --git a/src/multi_swarm_core/multi_swarm_core/genome/prompt_library.py b/src/multi_swarm_core/multi_swarm_core/genome/prompt_library.py index dc654ab..6a8dee4 100644 --- a/src/multi_swarm_core/multi_swarm_core/genome/prompt_library.py +++ b/src/multi_swarm_core/multi_swarm_core/genome/prompt_library.py @@ -101,6 +101,10 @@ class PromptLibrary: domain_warnings: str = field(default="") # opzionale: warning di dominio (es. crypto 24/7) anti_patterns: str = field(default="") # NEW v3.1: lista esplicita di pattern da evitare output_priorities: str = field(default="") # NEW v3.1: trade-off espliciti di output + # NEW v3.2: firme formali (arity + range) di indicatori strategy-specific + # non noti al core. Iniettato in _SYSTEM_GRAMMAR_SPEC come estensione della + # lista "Leaf - indicatori" per consentire alla LLM di chiamarli correttamente. + custom_indicators_spec: str = field(default="") def __post_init__(self) -> None: if not self.styles: @@ -120,6 +124,7 @@ class PromptLibrary: ("domain_warnings", self.domain_warnings), ("anti_patterns", self.anti_patterns), ("output_priorities", self.output_priorities), + ("custom_indicators_spec", self.custom_indicators_spec), ): if not isinstance(value, str): raise PromptLibraryError( @@ -142,6 +147,7 @@ class PromptLibrary: domain_warnings="", anti_patterns="", output_priorities="", + custom_indicators_spec="", ) @classmethod @@ -204,6 +210,12 @@ class PromptLibrary: raise PromptLibraryError(f"anti_patterns deve essere stringa, non {type(anti_patterns_raw)}") if not isinstance(output_priorities_raw, str): raise PromptLibraryError(f"output_priorities deve essere stringa, non {type(output_priorities_raw)}") + # Parse new optional top-level field (v3.2) + custom_indicators_spec_raw = data.get("custom_indicators_spec", "") + if not isinstance(custom_indicators_spec_raw, str): + raise PromptLibraryError( + f"custom_indicators_spec deve essere stringa, non {type(custom_indicators_spec_raw)}" + ) return cls( styles=styles, @@ -214,6 +226,7 @@ class PromptLibrary: domain_warnings=domain_warnings, anti_patterns=anti_patterns_raw, output_priorities=output_priorities_raw, + custom_indicators_spec=custom_indicators_spec_raw, ) @property diff --git a/src/multi_swarm_core/tests/unit/test_hypothesis_agent.py b/src/multi_swarm_core/tests/unit/test_hypothesis_agent.py index 6100984..ca4233a 100644 --- a/src/multi_swarm_core/tests/unit/test_hypothesis_agent.py +++ b/src/multi_swarm_core/tests/unit/test_hypothesis_agent.py @@ -420,3 +420,48 @@ def test_build_system_prompt_skips_anti_patterns_and_priorities_when_empty(mocke system_msg = call_kwargs["system"] assert "ANTI-PATTERN" not in system_msg assert "PRIORITA' DI OUTPUT" not in system_msg + + +def test_build_system_prompt_includes_custom_indicators_spec(mocker): # type: ignore[no-untyped-def] + """custom_indicators_spec da PromptLibrary appare nel SYSTEM prompt con header.""" + fake_llm = mocker.MagicMock() + fake_llm.complete.return_value = CompletionResult( + text=VALID_STRATEGY_JSON, + input_tokens=200, + output_tokens=80, + tier=ModelTier.C, + model="qwen", + ) + lib = PromptLibrary( + styles={"physicist": "Pensa come un fisico."}, + focus={}, + custom_indicators_spec="CUSTOM_IND_X", + ) + agent = HypothesisAgent(llm=fake_llm, prompt_library=lib) + agent.propose(make_genome(), make_summary()) + call_kwargs = fake_llm.complete.call_args.kwargs + system_msg = call_kwargs["system"] + assert "INDICATORI CUSTOM" in system_msg + assert "CUSTOM_IND_X" in system_msg + + +def test_build_system_prompt_skips_custom_indicators_spec_when_empty(mocker): # type: ignore[no-untyped-def] + """custom_indicators_spec='' -> sezione assente nel SYSTEM prompt.""" + fake_llm = mocker.MagicMock() + fake_llm.complete.return_value = CompletionResult( + text=VALID_STRATEGY_JSON, + input_tokens=200, + output_tokens=80, + tier=ModelTier.C, + model="qwen", + ) + lib = PromptLibrary( + styles={"physicist": "Pensa come un fisico."}, + focus={}, + custom_indicators_spec="", + ) + agent = HypothesisAgent(llm=fake_llm, prompt_library=lib) + agent.propose(make_genome(), make_summary()) + call_kwargs = fake_llm.complete.call_args.kwargs + system_msg = call_kwargs["system"] + assert "INDICATORI CUSTOM" not in system_msg diff --git a/src/multi_swarm_core/tests/unit/test_prompt_library.py b/src/multi_swarm_core/tests/unit/test_prompt_library.py index 958f468..49e78b1 100644 --- a/src/multi_swarm_core/tests/unit/test_prompt_library.py +++ b/src/multi_swarm_core/tests/unit/test_prompt_library.py @@ -90,6 +90,28 @@ def test_from_json_loads_anti_patterns_and_output_priorities(tmp_path: Path) -> assert lib.output_priorities == "Robustezza > ottimalita." +def test_from_json_loads_custom_indicators_spec(tmp_path: Path) -> None: + """from_json() legge custom_indicators_spec (v3.2): firme di indicatori strategy-specific.""" + data = { + "styles": {"physicist": {"directive": "Cerca leggi conservative."}}, + "custom_indicators_spec": ( + "candle_pattern: params=[length, sym0, sym1, ...], length in [3,12], " + "sym in {0=U,1=D,2=doji}\n" + "pythagorean_ratio: params=[lookback], lookback in [12,200]" + ), + } + lib = PromptLibrary.from_json(_write_json(data, tmp_path)) + assert "candle_pattern" in lib.custom_indicators_spec + assert "lookback in [12,200]" in lib.custom_indicators_spec + + +def test_from_json_defaults_custom_indicators_spec_when_absent(tmp_path: Path) -> None: + """custom_indicators_spec assente -> default stringa vuota.""" + data = {"styles": {"physicist": {"directive": "Cerca leggi conservative."}}} + lib = PromptLibrary.from_json(_write_json(data, tmp_path)) + assert lib.custom_indicators_spec == "" + + def test_strategy_crypto_directives_ascii_safe() -> None: """REGRESSION GUARD: nessuna directive contiene caratteri > U+007F. diff --git a/src/strategy_pythagoras/strategy_pythagoras/prompts.json b/src/strategy_pythagoras/strategy_pythagoras/prompts.json index ef2988b..4f25222 100644 --- a/src/strategy_pythagoras/strategy_pythagoras/prompts.json +++ b/src/strategy_pythagoras/strategy_pythagoras/prompts.json @@ -4,7 +4,9 @@ "_changelog": "v1.0 - Initial release. Schema clonato da strategy_crypto v3.2 con contenuto Pythagoras-aligned.", "_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. 4 per stile. Devono includere almeno 1 dei 3 indicatori Pythagoras (candle_pattern, pythagorean_ratio, fractal_mirror).", "_design_invariants": "(1) ASCII-safe; (2) Archetipo dominante: come ancora; (3) Lookback range esplicito; (4) Prima frase 'Il mercato e ...'; (5) Lunghezza directive 800-950 char.", - "_param_encoding_note": "Vincolo grammar: tutti i params sono float. candle_pattern: [length, sym0, sym1, ...] (sym: 0=U up close>open, 1=D down closeopen, 1=D down close= 1.0.\n Confronta con literal vicini a phi=1.618, 1/phi=0.618 (in realta usa 1.618 perche ratio >= 1), sqrt2=1.414, pi/2=1.571, e=2.718.\n\n {\"kind\": \"indicator\", \"name\": \"fractal_mirror\", \"params\": [k, axis_int]}\n k: int in [3,12] (lunghezza finestra di confronto)\n axis_int: int in {0, 1} (0=h mirror temporale, 1=v mirror prezzo)\n Esempio: mirror temporale su 8 candele = params=[8, 0]\n Esempio: mirror prezzo su 6 candele = params=[6, 1]\n Output: correlation di Pearson in [-1.0, 1.0] tra finestra e suo mirror. Confronta con literal in (-1, 1), tipicamente |0.5|-|0.8|.\n\nESEMPI di confronti CORRETTI:\n candle_pattern di 3 candele U-D-U attiva: {\"op\": \"eq\", \"args\": [{\"kind\":\"indicator\",\"name\":\"candle_pattern\",\"params\":[3,0,1,0]}, {\"kind\":\"literal\",\"value\":1.0}]}\n ratio phi entro 0.5%: {\"op\": \"and\", \"args\": [{\"op\":\"gt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.610}]}, {\"op\":\"lt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.626}]}]}\n mirror temporale forte: {\"op\": \"gt\", \"args\": [{\"kind\":\"indicator\",\"name\":\"fractal_mirror\",\"params\":[8,0]}, {\"kind\":\"literal\",\"value\":0.7}]}\n\nESEMPI ERRATI (dead branch o validation error):\n candle_pattern con stringa: params=[\"UDU\"] (errato: params devono essere numeri)\n candle_pattern: params=[3, \"U\", \"D\", \"U\"] (errato: usa codici 0/1/2)\n pythagorean_ratio: params=[\"close\"] (errato: params devono essere numeri, non nomi feature)\n pythagorean_ratio: params=[5] (errato: lookback < 12)\n pythagorean_ratio: params=[89, 1.618] (errato: arity 1, non 2)\n fractal_mirror: params=[0, 1] (errato: k < 3)\n fractal_mirror: params=[100, 0] (errato: k > 12)\n fractal_mirror: params=[8] (errato: arity 2, manca axis_int)", "agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo che cerca pattern frattali ricorrenti sui mercati crypto secondo il framework Pythagoras-Malanga (frattali H-C, trasformata di Fourier, geometria Evideon). Sei parte di una popolazione che esplora collettivamente lo spazio dei pattern: la diversita delle ipotesi e un asset critico. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili. La strategia che produci deve essere riconoscibile come emanata dal tuo stile.",