|
2 | 2 | from typing import Any, ClassVar, List, Optional |
3 | 3 | from unittest.mock import patch |
4 | 4 |
|
| 5 | +import httpx |
| 6 | +import openai |
5 | 7 | from pydantic import BaseModel, ConfigDict, ValidationError |
6 | 8 | import pytest |
7 | 9 |
|
8 | 10 | from rustic_ai.core.guild.agent_ext.depends.dependency_resolver import DependencySpec |
9 | 11 | from rustic_ai.core.guild.agent_ext.depends.llm.models import ( |
10 | 12 | AssistantMessage, |
| 13 | + ChatCompletionError, |
11 | 14 | ChatCompletionMessageToolCall, |
12 | 15 | ChatCompletionRequest, |
13 | 16 | ChatCompletionResponse, |
14 | 17 | Choice, |
15 | 18 | CompletionUsage, |
16 | 19 | FinishReason, |
17 | 20 | FunctionCall, |
| 21 | + ResponseCodes, |
18 | 22 | SystemMessage, |
19 | 23 | ToolType, |
20 | 24 | UserMessage, |
@@ -570,6 +574,154 @@ def test_agent_simple_response(self, generator, build_message_from_payload): |
570 | 574 | assert "react_trace" in provider_fields |
571 | 575 | assert len(provider_fields["react_trace"]) == 0 # No tool calls |
572 | 576 |
|
| 577 | + def test_provider_error_is_emitted_as_typed_error(self, generator, build_message_from_payload): |
| 578 | + agent_spec: AgentSpec = ( |
| 579 | + AgentBuilder(ReActAgent) |
| 580 | + .set_id("react_agent") |
| 581 | + .set_name("ReAct Agent") |
| 582 | + .set_description("A ReAct agent for testing") |
| 583 | + .set_properties( |
| 584 | + ReActAgentConfig( |
| 585 | + model="test-model", |
| 586 | + toolset=CalculatorToolset(), |
| 587 | + ) |
| 588 | + ) |
| 589 | + .build_spec() |
| 590 | + ) |
| 591 | + agent, results = wrap_agent_for_testing( |
| 592 | + agent_spec, |
| 593 | + dependency_map={ |
| 594 | + "llm": DependencySpec( |
| 595 | + class_name="rustic_ai.litellm.agent_ext.llm.LiteLLMResolver", |
| 596 | + properties={"model": "test-model"}, |
| 597 | + ) |
| 598 | + }, |
| 599 | + ) |
| 600 | + provider_error = openai.RateLimitError( |
| 601 | + "You have no credits remaining. PRIVATE_PROVIDER_DETAIL", |
| 602 | + response=httpx.Response( |
| 603 | + 429, |
| 604 | + request=httpx.Request("POST", "https://provider.invalid/chat"), |
| 605 | + json={"error": {"code": "insufficient_quota"}}, |
| 606 | + ), |
| 607 | + body={"error": {"code": "insufficient_quota"}}, |
| 608 | + ) |
| 609 | + |
| 610 | + with patch.object(agent, "_call_llm_direct", side_effect=provider_error): |
| 611 | + agent._on_message( |
| 612 | + build_message_from_payload( |
| 613 | + generator, |
| 614 | + ChatCompletionRequest(messages=[UserMessage(content="PRIVATE_USER_PROMPT")]), |
| 615 | + ) |
| 616 | + ) |
| 617 | + |
| 618 | + assert len(results) == 1 |
| 619 | + assert results[0].format.endswith(".ChatCompletionError") |
| 620 | + error = ChatCompletionError.model_validate(results[0].payload) |
| 621 | + assert error.status_code == ResponseCodes.RATE_LIMIT_ERROR |
| 622 | + assert error.body == {"error": {"code": "insufficient_quota"}} |
| 623 | + assert error.request_messages[0].content == "PRIVATE_USER_PROMPT" |
| 624 | + |
| 625 | + @pytest.mark.parametrize( |
| 626 | + ("provider_error", "expected_status"), |
| 627 | + [ |
| 628 | + ( |
| 629 | + openai.APIConnectionError( |
| 630 | + message="PRIVATE_CONNECTION_DETAIL", |
| 631 | + request=httpx.Request("POST", "https://provider.invalid/chat"), |
| 632 | + ), |
| 633 | + ResponseCodes.API_CONNECTION_ERROR, |
| 634 | + ), |
| 635 | + ( |
| 636 | + openai.APITimeoutError( |
| 637 | + request=httpx.Request("POST", "https://provider.invalid/chat") |
| 638 | + ), |
| 639 | + ResponseCodes.API_TIMEOUT_ERROR, |
| 640 | + ), |
| 641 | + ], |
| 642 | + ) |
| 643 | + def test_provider_transport_error_is_emitted_as_typed_error( |
| 644 | + self, |
| 645 | + generator, |
| 646 | + build_message_from_payload, |
| 647 | + provider_error, |
| 648 | + expected_status, |
| 649 | + ): |
| 650 | + agent_spec: AgentSpec = ( |
| 651 | + AgentBuilder(ReActAgent) |
| 652 | + .set_id("react_agent") |
| 653 | + .set_name("ReAct Agent") |
| 654 | + .set_description("A ReAct agent for testing") |
| 655 | + .set_properties( |
| 656 | + ReActAgentConfig( |
| 657 | + model="test-model", |
| 658 | + toolset=CalculatorToolset(), |
| 659 | + ) |
| 660 | + ) |
| 661 | + .build_spec() |
| 662 | + ) |
| 663 | + agent, results = wrap_agent_for_testing( |
| 664 | + agent_spec, |
| 665 | + dependency_map={ |
| 666 | + "llm": DependencySpec( |
| 667 | + class_name="rustic_ai.litellm.agent_ext.llm.LiteLLMResolver", |
| 668 | + properties={"model": "test-model"}, |
| 669 | + ) |
| 670 | + }, |
| 671 | + ) |
| 672 | + |
| 673 | + with patch.object(agent, "_call_llm_direct", side_effect=provider_error): |
| 674 | + agent._on_message( |
| 675 | + build_message_from_payload( |
| 676 | + generator, |
| 677 | + ChatCompletionRequest(messages=[UserMessage(content="PRIVATE_USER_PROMPT")]), |
| 678 | + ) |
| 679 | + ) |
| 680 | + |
| 681 | + assert len(results) == 1 |
| 682 | + assert results[0].format.endswith(".ChatCompletionError") |
| 683 | + error = ChatCompletionError.model_validate(results[0].payload) |
| 684 | + assert error.status_code == expected_status |
| 685 | + assert error.request_messages[0].content == "PRIVATE_USER_PROMPT" |
| 686 | + |
| 687 | + def test_unexpected_error_is_emitted_once_as_internal_error(self, generator, build_message_from_payload): |
| 688 | + agent_spec: AgentSpec = ( |
| 689 | + AgentBuilder(ReActAgent) |
| 690 | + .set_id("react_agent") |
| 691 | + .set_name("ReAct Agent") |
| 692 | + .set_description("A ReAct agent for testing") |
| 693 | + .set_properties( |
| 694 | + ReActAgentConfig( |
| 695 | + model="test-model", |
| 696 | + toolset=CalculatorToolset(), |
| 697 | + ) |
| 698 | + ) |
| 699 | + .build_spec() |
| 700 | + ) |
| 701 | + agent, results = wrap_agent_for_testing( |
| 702 | + agent_spec, |
| 703 | + dependency_map={ |
| 704 | + "llm": DependencySpec( |
| 705 | + class_name="rustic_ai.litellm.agent_ext.llm.LiteLLMResolver", |
| 706 | + properties={"model": "test-model"}, |
| 707 | + ) |
| 708 | + }, |
| 709 | + ) |
| 710 | + |
| 711 | + with patch.object(agent, "_call_llm_direct", side_effect=RuntimeError("PRIVATE_INTERNAL_DETAIL")): |
| 712 | + agent._on_message( |
| 713 | + build_message_from_payload( |
| 714 | + generator, |
| 715 | + ChatCompletionRequest(messages=[UserMessage(content="PRIVATE_USER_PROMPT")]), |
| 716 | + ) |
| 717 | + ) |
| 718 | + |
| 719 | + assert len(results) == 1 |
| 720 | + assert results[0].format.endswith(".ChatCompletionError") |
| 721 | + error = ChatCompletionError.model_validate(results[0].payload) |
| 722 | + assert error.status_code == ResponseCodes.INTERNAL_SERVER_ERROR |
| 723 | + assert error.message == "Error in ReAct loop: PRIVATE_INTERNAL_DETAIL" |
| 724 | + |
573 | 725 | def test_agent_with_tool_call(self, generator, build_message_from_payload): |
574 | 726 | """Test agent with tool calls.""" |
575 | 727 | agent_spec: AgentSpec = ( |
|
0 commit comments