Skip to content

Commit 5333572

Browse files
authored
Merge pull request #188 from ansari-project/fix/ayah-claude-and-usul-api-migration
Fix ayah-claude endpoint and migrate to new Usul API URL
2 parents e4d2420 + 228dec5 commit 5333572

File tree

5 files changed

+258
-14
lines changed

5 files changed

+258
-14
lines changed

src/ansari/agents/ansari_claude.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,20 @@
2222
class AnsariClaude(Ansari):
2323
"""Claude-based implementation of the Ansari agent."""
2424

25-
def __init__(self, settings: Settings, message_logger: MessageLogger = None, json_format=False):
25+
def __init__(self, settings: Settings, message_logger: MessageLogger = None, json_format=False, system_prompt_file=None):
2626
"""Initialize the Claude-based Ansari agent.
2727
2828
Args:
2929
settings: Application settings
3030
message_logger: Optional message logger instance
3131
json_format: Whether to use JSON format for responses
32+
system_prompt_file: Optional system prompt file name (defaults to 'system_msg_claude')
3233
"""
3334
# Call parent initialization
3435
super().__init__(settings, message_logger, json_format)
36+
37+
# Set the system prompt file to use (can be overridden for specific use cases like ayah endpoint)
38+
self.system_prompt_file = system_prompt_file or "system_msg_claude"
3539

3640
# Log environment information for debugging
3741
try:
@@ -679,7 +683,7 @@ def process_one_round(self) -> Generator[str, None, None]:
679683
# 1. API REQUEST PREPARATION AND EXECUTION
680684
# ======================================================================
681685
prompt_mgr = PromptMgr()
682-
system_prompt = prompt_mgr.bind("system_msg_claude").render()
686+
system_prompt = prompt_mgr.bind(self.system_prompt_file).render()
683687

684688
# Run pre-flight validation to ensure proper tool_use/tool_result relationship
685689
# This helps prevent API errors by fixing message structure before sending

src/ansari/app/main_api.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,19 +1131,10 @@ async def answer_ayah_question_claude(
11311131
# Create AnsariClaude instance with ayah-specific system prompt
11321132
logger.debug(f"Creating AnsariClaude instance for {req.surah}:{req.ayah}")
11331133

1134-
# Load the ayah-specific system prompt
1135-
system_prompt_path = os.path.join(
1136-
os.path.dirname(__file__), "..", "system_prompts", settings.AYAH_SYSTEM_PROMPT_FILE_NAME
1137-
)
1138-
1139-
with open(system_prompt_path, "r") as f:
1140-
ayah_system_prompt = f.read()
1141-
1142-
# Initialize AnsariClaude with the ayah-specific system prompt
1134+
# Initialize AnsariClaude with the ayah-specific system prompt file
11431135
ansari_claude = AnsariClaude(
11441136
settings,
1145-
system_prompt=ayah_system_prompt,
1146-
source_type=SourceType.WEB, # Using WEB for now, could add QURAN_COM if needed
1137+
system_prompt_file=settings.AYAH_SYSTEM_PROMPT_FILE_NAME
11471138
)
11481139

11491140
# Prepare the context with ayah information

src/ansari/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def get_resource_path(filename):
167167

168168
# Usul.ai API settings
169169
USUL_API_TOKEN: SecretStr = Field(default="") # Set via environment variable
170-
USUL_BASE_URL: str = Field(default="https://semantic-search.usul.ai/v1/vector-search")
170+
USUL_BASE_URL: str = Field(default="https://api.usul.ai/v1/vector-search")
171171
USUL_TOOL_NAME_PREFIX: str = Field(default="search_usul")
172172
TAFSIR_ENCYC_BOOK_ID: str = Field(default="pet7s2sjr900zvxjsafa3s3b")
173173
TAFSIR_ENCYC_VERSION_ID: str = Field(default="MT3i8pDNoM")
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Unit tests for AnsariClaude system_prompt_file parameter."""
2+
3+
import pytest
4+
from unittest.mock import MagicMock, patch, mock_open
5+
import anthropic
6+
7+
8+
@pytest.fixture
9+
def mock_settings():
10+
"""Mock settings for testing."""
11+
settings = MagicMock()
12+
settings.ANTHROPIC_API_KEY = MagicMock()
13+
settings.ANTHROPIC_API_KEY.get_secret_value.return_value = "test-api-key"
14+
settings.ANTHROPIC_MODEL = "claude-3-opus-20240229"
15+
settings.KALEMAT_API_KEY = MagicMock()
16+
settings.KALEMAT_API_KEY.get_secret_value.return_value = "test-kalemat-key"
17+
settings.VECTARA_API_KEY = MagicMock()
18+
settings.VECTARA_API_KEY.get_secret_value.return_value = "test-vectara-key"
19+
settings.MAWSUAH_VECTARA_CORPUS_KEY = "test-corpus-key"
20+
settings.TAFSIR_VECTARA_CORPUS_KEY = "test-tafsir-key"
21+
settings.USUL_API_TOKEN = MagicMock()
22+
settings.USUL_API_TOKEN.get_secret_value.return_value = "test-usul-token"
23+
settings.MODEL = "test-model"
24+
settings.PROMPT_PATH = "/test/prompts"
25+
settings.SYSTEM_PROMPT_FILE_NAME = "system_msg_default"
26+
settings.AYAH_SYSTEM_PROMPT_FILE_NAME = "system_msg_ayah"
27+
return settings
28+
29+
30+
class TestAnsariClaudeSystemPrompt:
31+
"""Test AnsariClaude system prompt file parameter."""
32+
33+
def test_default_system_prompt_file(self, mock_settings):
34+
"""Test that AnsariClaude uses default system prompt file when not specified."""
35+
with patch("anthropic.Anthropic"), \
36+
patch("src.ansari.util.prompt_mgr.PromptMgr") as mock_prompt_mgr:
37+
# Mock the prompt manager
38+
mock_prompt = MagicMock()
39+
mock_prompt.render.return_value = "Test system prompt"
40+
mock_prompt_mgr.return_value.bind.return_value = mock_prompt
41+
42+
from src.ansari.agents.ansari_claude import AnsariClaude
43+
44+
# Initialize without system_prompt_file parameter
45+
ansari = AnsariClaude(mock_settings)
46+
47+
# Verify default is used
48+
assert ansari.system_prompt_file == "system_msg_claude"
49+
50+
def test_custom_system_prompt_file(self, mock_settings):
51+
"""Test that AnsariClaude uses custom system prompt file when specified."""
52+
with patch("anthropic.Anthropic"), \
53+
patch("src.ansari.util.prompt_mgr.PromptMgr") as mock_prompt_mgr:
54+
# Mock the prompt manager
55+
mock_prompt = MagicMock()
56+
mock_prompt.render.return_value = "Test system prompt"
57+
mock_prompt_mgr.return_value.bind.return_value = mock_prompt
58+
59+
from src.ansari.agents.ansari_claude import AnsariClaude
60+
61+
# Initialize with custom system_prompt_file
62+
ansari = AnsariClaude(mock_settings, system_prompt_file="system_msg_ayah")
63+
64+
# Verify custom file is used
65+
assert ansari.system_prompt_file == "system_msg_ayah"
66+
67+
def test_system_prompt_loaded_in_process_one_round(self, mock_settings):
68+
"""Test that the correct system prompt file is loaded during process_one_round."""
69+
with patch("anthropic.Anthropic") as mock_anthropic:
70+
# Mock the Anthropic client
71+
mock_client = MagicMock()
72+
mock_anthropic.return_value = mock_client
73+
74+
# Mock the response stream
75+
mock_response = MagicMock()
76+
mock_response.__iter__ = MagicMock(return_value=iter([]))
77+
mock_client.messages.create.return_value = mock_response
78+
79+
from src.ansari.agents.ansari_claude import AnsariClaude
80+
from src.ansari.util.prompt_mgr import PromptMgr
81+
82+
# Initialize with custom system prompt file
83+
ansari = AnsariClaude(mock_settings, system_prompt_file="system_msg_ayah")
84+
85+
# Add a message to history
86+
ansari.message_history = [{"role": "user", "content": "test question"}]
87+
88+
# Mock PromptMgr to verify the correct file is loaded
89+
with patch.object(PromptMgr, 'bind') as mock_bind:
90+
mock_prompt = MagicMock()
91+
mock_prompt.render.return_value = "Test ayah system prompt"
92+
mock_bind.return_value = mock_prompt
93+
94+
# Call process_one_round
95+
result = list(ansari.process_one_round())
96+
97+
# Verify the correct system prompt file was loaded
98+
mock_bind.assert_called_with("system_msg_ayah")
99+
100+
def test_ayah_endpoint_initialization(self, mock_settings):
101+
"""Test that the ayah-claude endpoint can initialize AnsariClaude with custom system prompt."""
102+
with patch("anthropic.Anthropic"):
103+
from src.ansari.agents.ansari_claude import AnsariClaude
104+
105+
# Simulate ayah endpoint initialization
106+
ansari = AnsariClaude(
107+
mock_settings,
108+
system_prompt_file=mock_settings.AYAH_SYSTEM_PROMPT_FILE_NAME
109+
)
110+
111+
# Verify the ayah system prompt file is used
112+
assert ansari.system_prompt_file == "system_msg_ayah"
113+
114+
def test_process_one_round_with_different_prompts(self, mock_settings):
115+
"""Test that different system prompts are used based on initialization."""
116+
with patch("anthropic.Anthropic") as mock_anthropic:
117+
# Mock the Anthropic client
118+
mock_client = MagicMock()
119+
mock_anthropic.return_value = mock_client
120+
121+
# Mock the response stream
122+
mock_response = MagicMock()
123+
mock_response.__iter__ = MagicMock(return_value=iter([]))
124+
mock_client.messages.create.return_value = mock_response
125+
126+
from src.ansari.agents.ansari_claude import AnsariClaude
127+
from src.ansari.util.prompt_mgr import PromptMgr
128+
129+
# Test with default prompt
130+
ansari_default = AnsariClaude(mock_settings)
131+
ansari_default.message_history = [{"role": "user", "content": "test"}]
132+
133+
with patch.object(PromptMgr, 'bind') as mock_bind:
134+
mock_prompt = MagicMock()
135+
mock_prompt.render.return_value = "Default system prompt"
136+
mock_bind.return_value = mock_prompt
137+
138+
list(ansari_default.process_one_round())
139+
mock_bind.assert_called_with("system_msg_claude")
140+
141+
# Test with ayah prompt
142+
ansari_ayah = AnsariClaude(mock_settings, system_prompt_file="system_msg_ayah")
143+
ansari_ayah.message_history = [{"role": "user", "content": "test"}]
144+
145+
with patch.object(PromptMgr, 'bind') as mock_bind:
146+
mock_prompt = MagicMock()
147+
mock_prompt.render.return_value = "Ayah system prompt"
148+
mock_bind.return_value = mock_prompt
149+
150+
list(ansari_ayah.process_one_round())
151+
mock_bind.assert_called_with("system_msg_ayah")

tests/unit/test_ayah_claude_fix.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Integration test to verify the ayah-claude endpoint fix."""
2+
3+
from unittest.mock import patch, MagicMock
4+
import os
5+
import tempfile
6+
7+
8+
def test_ayah_claude_endpoint_loads_correct_system_prompt():
9+
"""Test that the ayah-claude endpoint loads the system prompt correctly using PromptMgr."""
10+
11+
# Create a temporary system prompt file
12+
with tempfile.TemporaryDirectory() as tmpdir:
13+
# Create the prompts directory structure
14+
prompts_dir = os.path.join(tmpdir, "prompts")
15+
os.makedirs(prompts_dir)
16+
17+
# Create test prompt files
18+
default_prompt_file = os.path.join(prompts_dir, "system_msg_default.txt")
19+
claude_prompt_file = os.path.join(prompts_dir, "system_msg_claude.txt")
20+
ayah_prompt_file = os.path.join(prompts_dir, "system_msg_ayah.txt")
21+
22+
with open(default_prompt_file, "w") as f:
23+
f.write("Default system prompt")
24+
with open(claude_prompt_file, "w") as f:
25+
f.write("Claude system prompt")
26+
with open(ayah_prompt_file, "w") as f:
27+
f.write("Ayah system prompt for Quranic questions")
28+
29+
# Mock settings
30+
mock_settings = MagicMock()
31+
mock_settings.ANTHROPIC_API_KEY.get_secret_value.return_value = "test-key"
32+
mock_settings.ANTHROPIC_MODEL = "claude-3-opus-20240229"
33+
mock_settings.KALEMAT_API_KEY.get_secret_value.return_value = "test-key"
34+
mock_settings.VECTARA_API_KEY.get_secret_value.return_value = "test-key"
35+
mock_settings.USUL_API_TOKEN.get_secret_value.return_value = "test-key"
36+
mock_settings.MAWSUAH_VECTARA_CORPUS_KEY = "test-corpus"
37+
mock_settings.TAFSIR_VECTARA_CORPUS_KEY = "test-tafsir"
38+
mock_settings.MODEL = "test-model"
39+
mock_settings.PROMPT_PATH = prompts_dir
40+
mock_settings.SYSTEM_PROMPT_FILE_NAME = "system_msg_default"
41+
mock_settings.AYAH_SYSTEM_PROMPT_FILE_NAME = "system_msg_ayah"
42+
43+
# Patch Anthropic client
44+
with patch("anthropic.Anthropic") as mock_anthropic:
45+
mock_client = MagicMock()
46+
mock_anthropic.return_value = mock_client
47+
48+
from src.ansari.agents.ansari_claude import AnsariClaude
49+
50+
# Test 1: Default initialization should use system_msg_claude
51+
ansari_default = AnsariClaude(mock_settings)
52+
assert ansari_default.system_prompt_file == "system_msg_claude"
53+
54+
# Test 2: Initialization with ayah system prompt
55+
ansari_ayah = AnsariClaude(
56+
mock_settings,
57+
system_prompt_file=mock_settings.AYAH_SYSTEM_PROMPT_FILE_NAME
58+
)
59+
assert ansari_ayah.system_prompt_file == "system_msg_ayah"
60+
61+
# Test 3: Verify process_one_round uses the correct prompt file
62+
# Mock the API response
63+
mock_response = MagicMock()
64+
mock_response.__iter__ = MagicMock(return_value=iter([]))
65+
mock_client.messages.create.return_value = mock_response
66+
67+
# Add a message to process
68+
ansari_ayah.message_history = [{"role": "user", "content": "test question"}]
69+
70+
# Process the message
71+
list(ansari_ayah.process_one_round())
72+
73+
# Verify the API was called with the ayah system prompt
74+
api_call_args = mock_client.messages.create.call_args
75+
if api_call_args:
76+
system_prompt = api_call_args.kwargs.get("system", [{}])[0].get("text", "")
77+
# The system prompt should be loaded from the ayah file
78+
# We can't check the exact content without actually loading the file,
79+
# but we can verify the API was called
80+
assert mock_client.messages.create.called
81+
82+
83+
def test_ansari_claude_accepts_system_prompt_file_parameter():
84+
"""Test that AnsariClaude constructor accepts system_prompt_file parameter."""
85+
86+
# This test verifies the signature change without needing to mock all dependencies
87+
from inspect import signature
88+
from src.ansari.agents.ansari_claude import AnsariClaude
89+
90+
# Get the signature of the __init__ method
91+
sig = signature(AnsariClaude.__init__)
92+
93+
# Check that system_prompt_file is a parameter
94+
assert "system_prompt_file" in sig.parameters
95+
96+
# Check that it has a default value of None
97+
param = sig.parameters["system_prompt_file"]
98+
assert param.default is None

0 commit comments

Comments
 (0)