From 9f86b2bee3be92974aa98013e5b5c27c2df821bd Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 12 Jun 2026 19:01:50 -0400 Subject: [PATCH] feat(ai_client): add send_result() public API returning Result[str] --- config.toml | 22 ++++---- manualslop_layout.ini | 22 ++++---- project_history.toml | 2 +- src/ai_client.py | 89 +++++++++++++++++++++++++++++- tests/test_ai_client_result.py | 4 +- tests/test_deprecation_warnings.py | 2 +- 6 files changed, 114 insertions(+), 27 deletions(-) diff --git a/config.toml b/config.toml index 09148a1e..7f01343b 100644 --- a/config.toml +++ b/config.toml @@ -26,7 +26,7 @@ separate_tool_calls_panel = true bg_shader_enabled = false crt_filter_enabled = false separate_task_dag = false -separate_usage_analytics = true +separate_usage_analytics = false separate_tier1 = false separate_tier2 = false separate_tier3 = false @@ -53,7 +53,7 @@ separate_external_tools = false Message = false Response = false "Tool Calls" = true -"Text Viewer" = true +"Text Viewer" = false Theme = true "Log Management" = true Diagnostics = false @@ -75,26 +75,26 @@ brightness = 0.7699999809265137 contrast = 0.7200000286102295 gamma = 0.6899999976158142 -[theme.tone_mapping."Solarized Light"] -brightness = 0.4699999988079071 -contrast = 0.800000011920929 -gamma = 0.6700000166893005 +[theme.tone_mapping.Binks] +brightness = 0.47999998927116394 +contrast = 0.8399999737739563 +gamma = 2.2100000381469727 [theme.tone_mapping.solarized_light] brightness = 0.6899999976158142 contrast = 0.8600000143051147 gamma = 0.7699999809265137 +[theme.tone_mapping."Solarized Light"] +brightness = 0.4699999988079071 +contrast = 0.800000011920929 +gamma = 0.6700000166893005 + [theme.tone_mapping.moss] brightness = 0.7699999809265137 contrast = 0.8700000047683716 gamma = 1.0 -[theme.tone_mapping.Binks] -brightness = 0.47999998927116394 -contrast = 0.8399999737739563 -gamma = 2.2100000381469727 - [mma] max_workers = 4 diff --git a/manualslop_layout.ini b/manualslop_layout.ini index 3cf02280..7c6f5d21 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -57,7 +57,7 @@ DockId=0x00000010,5 [Window][Tool Calls] Pos=106,92 -Size=1560,1096 +Size=1560,1108 Collapsed=0 DockId=0x00000002,1 @@ -77,7 +77,7 @@ DockId=0xAFC85805,2 [Window][Theme] Pos=0,28 -Size=104,1160 +Size=104,1172 Collapsed=0 DockId=0x00000010,0 @@ -106,25 +106,25 @@ DockId=0x0000000D,0 [Window][Discussion Hub] Pos=106,92 -Size=1560,1096 +Size=1560,1108 Collapsed=0 DockId=0x00000002,0 [Window][Operations Hub] Pos=0,28 -Size=104,1160 +Size=104,1172 Collapsed=0 DockId=0x00000010,4 [Window][Files & Media] Pos=0,28 -Size=104,1160 +Size=104,1172 Collapsed=0 DockId=0x00000010,2 [Window][AI Settings] Pos=0,28 -Size=104,1160 +Size=104,1172 Collapsed=0 DockId=0x00000010,3 @@ -410,7 +410,7 @@ DockId=0x00000002,1 [Window][Project Settings] Pos=0,28 -Size=104,1160 +Size=104,1172 Collapsed=0 DockId=0x00000010,1 @@ -541,8 +541,8 @@ Size=186,192 Collapsed=0 [Window][###Text_Viewer_Unified] -Pos=443,513 -Size=1021,1293 +Pos=466,615 +Size=1021,918 Collapsed=0 [Table][0xFB6E3870,4] @@ -870,11 +870,11 @@ Column 4 Weight=1.0000 DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1666,1160 Split=X +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1666,1172 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1426,1681 Split=Y Selected=0x3F1379AF - DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x7BD57D6A + DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x418C7449 DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1560,1681 Split=Y Selected=0x6F2B5B04 DockNode ID=0x00000001 Parent=0x00000006 SizeRef=1560,107 Selected=0x2C0206CE diff --git a/project_history.toml b/project_history.toml index a45b94b9..ecacc583 100644 --- a/project_history.toml +++ b/project_history.toml @@ -9,5 +9,5 @@ active = "main" [discussions.main] git_commit = "" -last_updated = "2026-06-11T21:21:04" +last_updated = "2026-06-12T06:55:16" history = [] diff --git a/src/ai_client.py b/src/ai_client.py index 97005fd9..db978763 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -61,7 +61,7 @@ PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minima # existing call sites and the T3.1 test (which asserts # hasattr(src.ai_client, '_require_warmed')) continue to work. from src.module_loader import _require_warmed # noqa: E402,F401 -from src.result_types import ErrorInfo, ErrorKind # noqa: E402,F401 +from src.result_types import ErrorInfo, ErrorKind, Result # noqa: E402,F401 _provider: str = "gemini" @@ -2768,6 +2768,93 @@ def send( if monitor.enabled: monitor.end_component("ai_client.send") return res +def send_result( + md_content: str, + user_message: str, + base_dir: str = ".", + file_items: list[dict[str, Any]] | None = None, + discussion_history: str = "", + stream: bool = False, + pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, + qa_callback: Optional[Callable[[str], str]] = None, + enable_tools: bool = True, + stream_callback: Optional[Callable[[str], None]] = None, + patch_callback: Optional[Callable[[str, str], Optional[str]]] = None, + rag_engine: Optional[Any] = None, +) -> Result[str]: + """ + [NEW] The Result-based public API. Returns Result[str, ErrorInfo]. + data is the response text on success; errors contains ErrorInfo on failure. + [C: tests/test_ai_client_result.py:test_send_result_public_api_returns_result, tests/test_ai_client_result.py:test_send_result_preserves_errors, tests/test_deprecation_warnings.py:test_send_result_does_not_emit_deprecation] + """ + monitor = performance_monitor.get_monitor() + if monitor.enabled: monitor.start_component("ai_client.send_result") + + if rag_engine and getattr(rag_engine.config, "enabled", False) and "## Retrieved Context" not in user_message: + chunks = rag_engine.search(user_message) + if chunks: + context_block = "## Retrieved Context\n\n" + for i, chunk in enumerate(chunks): + path = chunk.get("metadata", {}).get("path", "unknown") + context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n" + user_message = context_block + user_message + + _append_comms("OUT", "request", {"message": user_message, "system": _get_combined_system_prompt(_active_tool_preset, _active_bias_profile)}) + with _send_lock: + p = str(_provider).lower().strip() + try: + if p == "gemini": + res = _send_gemini_result( + md_content, user_message, base_dir, file_items, discussion_history, + pre_tool_callback, qa_callback, enable_tools, stream_callback, patch_callback + ) + elif p == "gemini_cli": + res = _send_gemini_cli_result( + md_content, user_message, base_dir, file_items, discussion_history, + pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "anthropic": + res = _send_anthropic_result( + md_content, user_message, base_dir, file_items, discussion_history, + pre_tool_callback, qa_callback, stream_callback=stream_callback, patch_callback=patch_callback + ) + elif p == "deepseek": + res = _send_deepseek_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "minimax": + res = _send_minimax_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "qwen": + res = _send_qwen_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "llama": + res = _send_llama_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "grok": + res = _send_grok_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + elif p == "llama_native": + res = _send_llama_native_result( + md_content, user_message, base_dir, file_items, discussion_history, + stream, pre_tool_callback, qa_callback, stream_callback, patch_callback + ) + else: + res = Result(data="", errors=[ErrorInfo(kind=ErrorKind.CONFIG, message=f"unknown provider: {_provider}", source="ai_client.send_result")]) + except Exception as exc: + res = Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(exc), source="ai_client.send_result", original=exc)]) + if monitor.enabled: monitor.end_component("ai_client.send_result") + return res + def _add_bleed_derived(d: dict[str, Any], sys_tok: int = 0, tool_tok: int = 0) -> dict[str, Any]: """ [C: tests/test_token_viz.py:test_add_bleed_derived_aliases, tests/test_token_viz.py:test_add_bleed_derived_breakdown, tests/test_token_viz.py:test_add_bleed_derived_headroom, tests/test_token_viz.py:test_add_bleed_derived_headroom_clamped_to_zero, tests/test_token_viz.py:test_add_bleed_derived_history_clamped_to_zero, tests/test_token_viz.py:test_add_bleed_derived_would_trim_false, tests/test_token_viz.py:test_add_bleed_derived_would_trim_true, tests/test_token_viz.py:test_would_trim_boundary_exact, tests/test_token_viz.py:test_would_trim_just_above_threshold, tests/test_token_viz.py:test_would_trim_just_below_threshold] diff --git a/tests/test_ai_client_result.py b/tests/test_ai_client_result.py index 6a863300..525e5d86 100644 --- a/tests/test_ai_client_result.py +++ b/tests/test_ai_client_result.py @@ -6,7 +6,7 @@ from src.result_types import Result, ErrorInfo, ErrorKind def test_send_result_public_api_returns_result() -> None: with patch.object(ai_client, "set_provider"): - with patch.object(ai_client, "_send_anthropic_result", return_value=Result(data="hello")) as mock_send: + with patch.object(ai_client, "_send_gemini_result", return_value=Result(data="hello")) as mock_send: r = ai_client.send_result("system", "user") assert isinstance(r, Result) assert r.ok @@ -27,7 +27,7 @@ def test_send_deprecated_emits_warning() -> None: def test_send_result_preserves_errors() -> None: err = ErrorInfo(kind=ErrorKind.RATE_LIMIT, message="slow down", source="test") with patch.object(ai_client, "set_provider"): - with patch.object(ai_client, "_send_anthropic_result", return_value=Result(data="", errors=[err])): + with patch.object(ai_client, "_send_gemini_result", return_value=Result(data="", errors=[err])): r = ai_client.send_result("system", "user") assert not r.ok assert r.errors == [err] diff --git a/tests/test_deprecation_warnings.py b/tests/test_deprecation_warnings.py index 7a7a1638..c274633f 100644 --- a/tests/test_deprecation_warnings.py +++ b/tests/test_deprecation_warnings.py @@ -17,7 +17,7 @@ def test_send_deprecated_warning_emitted_once_per_site() -> None: def test_send_result_does_not_emit_deprecation() -> None: with patch.object(ai_client, "set_provider"): - with patch.object(ai_client, "_send_anthropic_result", return_value=Result(data="x")): + with patch.object(ai_client, "_send_gemini_result", return_value=Result(data="x")): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") ai_client.send_result("s", "u")