Skip to content

Fix issues in test_create_profile and changelog #2348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 55 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
631abea
Create test_create_profile.py
SongmingFan Mar 10, 2025
affae54
Bump version to 3.12.10
SongmingFan Mar 31, 2025
a4b534b
test tag publish
SongmingFan Mar 31, 2025
0a62c26
Test automatic tag creation and push
SongmingFan Mar 31, 2025
9f69feb
Test semantic versioning tag format
SongmingFan Mar 31, 2025
7b6231f
Remove automated Git tag scripts
SongmingFan Mar 31, 2025
cc3bfbd
Test automatic tag creation and push
SongmingFan Mar 31, 2025
f9dbf33
Test updated tag versioning hook
SongmingFan Mar 31, 2025
71a6b88
Test automatic tag creation and push
SongmingFan Mar 31, 2025
b463924
Test automatic tag creation and push
SongmingFan Mar 31, 2025
8db7ee7
Test automatic tag creation and push
SongmingFan Mar 31, 2025
84c28e0
Test automatic tag creation and push
SongmingFan Mar 31, 2025
5026193
Test automatic tag creation and push
SongmingFan Mar 31, 2025
1e0d4e4
Merge branch 'master' of https://github.com/PolicyEngine/policyengine…
SongmingFan Apr 7, 2025
6b31886
Fix issues in test_create_profile and changelog
SongmingFan Apr 7, 2025
a32468d
Post merge tag release
SongmingFan Apr 9, 2025
fbb2872
Github Action Automated Retry
SongmingFan Apr 21, 2025
935870f
Fixed implementation of create_test_profile.py and changelog entry fo…
SongmingFan Apr 30, 2025
05efebe
Update test_create_profile.py
SongmingFan May 9, 2025
2d54d1e
Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
4ac3748
Delete tests/unit/services/test_create_profile.py
SongmingFan123 May 12, 2025
4ea4f69
Delete .githook/post-merge.sh
SongmingFan123 May 12, 2025
0baf579
Fix #2348 Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
b17b110
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
ff59336
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
65b9d8d
Revert "Bump version to 3.12.10"
SongmingFan May 18, 2025
44fb61e
Update test_create_profile.py
SongmingFan May 9, 2025
d998438
Fix #2348 Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
ae3529f
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
8c03611
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
5c47cdf
Revert "Bump version to 3.12.10"
SongmingFan May 18, 2025
3e11b7f
Merge branch 'test-create-profile' of https://github.com/PolicyEngine…
SongmingFan May 22, 2025
1a00bbd
Fixed implementation of create_test_profile.py and changelog entry fo…
SongmingFan Apr 30, 2025
51fb6e8
Update test_create_profile.py
SongmingFan May 9, 2025
234db7d
Fix #2348 Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
a57b573
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
6a3fca6
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
becd650
Merge branch 'test-create-profile' of https://github.com/PolicyEngine…
SongmingFan May 22, 2025
5a5d793
Fixed implementation of create_test_profile.py and changelog entry fo…
SongmingFan Apr 30, 2025
7b3b43f
Update test_create_profile.py
SongmingFan May 9, 2025
cc8ca07
Fix #2348 Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
f2e11bc
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
23de16c
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
8e0cff1
Merge branch 'test-create-profile' of https://github.com/PolicyEngine…
SongmingFan May 22, 2025
042db93
Fixed implementation of create_test_profile.py and changelog entry fo…
SongmingFan Apr 30, 2025
e741f83
Update test_create_profile.py
SongmingFan May 9, 2025
31e8b0a
Fix #2348 Delete changelog_entry.yaml
SongmingFan123 May 12, 2025
a3f5f21
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
d2e02d1
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
ec79550
Merge branch 'test-create-profile' of https://github.com/PolicyEngine…
SongmingFan May 22, 2025
a735054
Fixed implementation of create_test_profile.py and changelog entry fo…
SongmingFan Apr 30, 2025
c539573
Update test_create_profile.py
SongmingFan May 9, 2025
94d99e1
Revert "Update test_create_profile.py"
SongmingFan May 16, 2025
a4d6f9f
Reapply "Update test_create_profile.py"
SongmingFan May 16, 2025
0558d51
Merge branch 'test-create-profile' of https://github.com/PolicyEngine…
SongmingFan May 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
4 changes: 0 additions & 4 deletions .github/publish-git-tag.sh

This file was deleted.

11 changes: 8 additions & 3 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ jobs:
credentials_json: "${{ secrets.GCP_SA_KEY }}"
- name: Set up GCloud
uses: "google-github-actions/setup-gcloud@v2"
- name: Deploy
run: make deploy
- name: Deploy with retries
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 10
retry_on: error
command: make deploy
env:
POLICYENGINE_DB_PASSWORD: ${{ secrets.POLICYENGINE_DB_PASSWORD }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
Expand All @@ -92,4 +97,4 @@ jobs:
- name: Build container
run: docker build -t ghcr.io/policyengine/policyengine docker
- name: Push container
run: docker push ghcr.io/policyengine/policyengine
run: docker push ghcr.io/policyengine/policyengine
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ test:
coverage xml -i

debug-test:
MAX_HOUSEHOLDS=1000 FLASK_DEBUG=1 pytest -vv --durations=0 tests
MAX_HOUSEHOLDS=1000 FLASK_DEBUG=1 pytest -vv --durations=0 $(TEST_PATH)

# Usage: make debug-test-file TEST_FILE=tests/unit/services/test_create_profile.py
debug-test-file:
MAX_HOUSEHOLDS=1000 FLASK_DEBUG=1 pytest -vv --durations=0 $(TEST_FILE)

format:
black . -l 79
Expand Down
Empty file removed changelog_entry.yaml
Empty file.
4 changes: 4 additions & 0 deletions policyengine_api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
POST = "POST"
UPDATE = "UPDATE"
LIST = "LIST"
<<<<<<< HEAD
VERSION = "3.15.7"
=======
VERSION = "3.12.9"
>>>>>>> parent of affae549 (Bump version to 3.12.10)
COUNTRIES = ("uk", "us", "ca", "ng", "il")
COUNTRY_PACKAGE_NAMES = (
"policyengine_uk",
Expand Down
127 changes: 101 additions & 26 deletions policyengine_api/services/ai_analysis_service.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import anthropic
import os
import json
from typing import Generator, Optional, Literal
from typing import Generator, Optional
from policyengine_api.data import local_database
from pydantic import BaseModel
import openai
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()


class StreamEvent(BaseModel):
Expand Down Expand Up @@ -42,40 +47,110 @@ def get_existing_analysis(self, prompt: str) -> Optional[str]:

return json.dumps(analysis["analysis"])

def trigger_ai_analysis(self, prompt: str) -> Generator[str, None, None]:

def _call_claude_api(self, prompt: str) -> Generator[StreamEvent, None, None]:
"""
Make an API call to Anthropic's Claude model and return a generator
of stream events
"""
# Configure a Claude client
claude_client = anthropic.Anthropic(
api_key=os.getenv("ANTHROPIC_API_KEY")
)

def generate():
response_text = ""
response_text = ""

with claude_client.messages.stream(
model="claude-3-5-sonnet-20240620",
max_tokens=1500,
temperature=0.0,
system="Respond with a historical quote",
messages=[{"role": "user", "content": prompt}],
) as stream:
for event in stream:
# Docs on structure of Anthropic error events at https://docs.anthropic.com/en/api/messages-streaming#error-events
if event.type == "error":
error: dict[str, str] = event.error
error_type: str = error["type"]
yield ErrorEvent(error=error_type)
return
if event.type == "text":
response_text += event.text
yield TextEvent(stream=event.text)

# Update the analysis record if no error occurred
local_database.query(
f"INSERT INTO analysis (prompt, analysis, status) VALUES (?, ?, ?)",
(prompt, response_text, "ok"),
)

with claude_client.messages.stream(
model="claude-3-5-sonnet-20240620",
max_tokens=1500,
temperature=0.0,
system="Respond with a historical quote",
def _call_openai_api(self, prompt: str) -> Generator[StreamEvent, None, None]:
"""
Make an API call to OpenAI's ChatGPT model and return a generator
of stream events
"""
# Configure OpenAI client
openai_client = openai.Client(
api_key=os.getenv("OPENAI_API_KEY")
)

response_text = ""

try:
# Create a streaming response
stream = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
) as stream:
for event in stream:
# Docs on structure of Anthropic error events at https://docs.anthropic.com/en/api/messages-streaming#error-events
if event.type == "error":
error: dict[str, str] = event.error
error_type: str = error["type"]
return_event = ErrorEvent(error=error_type)
yield json.dumps(return_event.model_dump()) + "\n"
return
if event.type == "text":
response_text += event.text
return_event = TextEvent(stream=event.text)
yield json.dumps(return_event.model_dump()) + "\n"

# Update the analysis record and return if no error occurred
temperature=0.0,
max_tokens=1500,
stream=True,
)

# Process the streaming response
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
response_text += content
yield TextEvent(stream=content)

# Update the analysis record if no error occurred
local_database.query(
f"INSERT INTO analysis (prompt, analysis, status) VALUES (?, ?, ?)",
(prompt, response_text, "ok"),
)

except openai.OpenAIError as e:
error_message = str(e)

# Check for quota/billing error
if "insufficient_quota" in error_message:
error_message = "OpenAI API quota exceeded or billing issue. Please check your OpenAI account."

yield ErrorEvent(error=error_message)
except Exception as e:
yield ErrorEvent(error=str(e))

def trigger_ai_analysis(self, prompt: str, provider: str = "claude") -> Generator[str, None, None]:
"""
Trigger AI analysis with the specified provider

Args:
prompt: The prompt to send to the AI model
provider: The AI provider to use ('claude' or 'openai')

Returns:
A generator that yields JSON-serialized stream events
"""
def generate():
stream_generator = None

if provider.lower() == "claude":
stream_generator = self._call_claude_api(prompt)
elif provider.lower() == "openai":
stream_generator = self._call_openai_api(prompt)
else:
yield json.dumps(ErrorEvent(error=f"Unsupported provider: {provider}").model_dump()) + "\n"
return

for event in stream_generator:
yield json.dumps(event.model_dump()) + "\n"

return generate()
139 changes: 139 additions & 0 deletions standalone_ai_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import os
import json
import anthropic
import openai
from dotenv import load_dotenv
from pydantic import BaseModel
from typing import Generator

# Load environment variables from .env file
load_dotenv()

# Stream event classes
class StreamEvent(BaseModel):
type: str

class TextEvent(StreamEvent):
type: str = "text"
stream: str

class ErrorEvent(StreamEvent):
type: str = "error"
error: str


class AIService:
"""Simplified AI service for testing purposes"""

def _call_claude_api(self, prompt: str) -> Generator[StreamEvent, None, None]:
"""
Make an API call to Anthropic's Claude model and return a generator
of stream events
"""
try:
# Configure a Claude client
claude_client = anthropic.Anthropic(
api_key=os.getenv("ANTHROPIC_API_KEY")
)

with claude_client.messages.stream(
model="claude-3-5-sonnet-20240620",
max_tokens=1500,
temperature=0.0,
system="Respond with a historical quote",
messages=[{"role": "user", "content": prompt}],
) as stream:
for event in stream:
if event.type == "error":
error: dict[str, str] = event.error
error_type: str = error["type"]
yield ErrorEvent(error=error_type)
return
if event.type == "text":
yield TextEvent(stream=event.text)
except Exception as e:
yield ErrorEvent(error=str(e))

def _call_openai_api(self, prompt: str) -> Generator[StreamEvent, None, None]:
try:
openai_client = openai.Client(
api_key=os.getenv("OPENAI_API_KEY")
)

stream = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=1500,
stream=True,
)

for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
yield TextEvent(stream=content)

except openai.OpenAIError as e:
error_message = str(e)
yield ErrorEvent(error=error_message)
except Exception as e:
yield ErrorEvent(error=str(e))

def trigger_ai_analysis(self, prompt: str, provider: str = "claude") -> Generator[str, None, None]:
"""
Trigger AI analysis with the specified provider

Args:
prompt: The prompt to send to the AI model
provider: The AI provider to use ('claude' or 'openai')

Returns:
A generator that yields JSON-serialized stream events
"""
def generate():
stream_generator = None

if provider.lower() == "claude":
stream_generator = self._call_claude_api(prompt)
elif provider.lower() == "openai":
stream_generator = self._call_openai_api(prompt)
else:
yield json.dumps(ErrorEvent(error=f"Unsupported provider: {provider}").model_dump()) + "\n"
return

for event in stream_generator:
yield json.dumps(event.model_dump()) + "\n"

return generate()


def test_service_provider(provider):
print(f"\nTesting AI Service with {provider} provider...")
service = AIService()
prompt = "What is the capital of France?"

try:
# Collect results
results = []
for response in service.trigger_ai_analysis(prompt, provider=provider):
print(response)
results.append(json.loads(response))

# Check for errors in results
errors = [r for r in results if r.get("type") == "error"]
if errors:
print(f"{provider} test completed with errors: {errors[0].get('error')}")
else:
print(f"{provider} test completed successfully.")

except Exception as e:
print(f"Error testing {provider}: {str(e)}")


if __name__ == "__main__":
print("API Key Verification:")
print(f"ANTHROPIC_API_KEY: {'Present' if os.getenv('ANTHROPIC_API_KEY') else 'Missing'}")
print(f"OPENAI_API_KEY: {'Present' if os.getenv('OPENAI_API_KEY') else 'Missing'}")

test_service_provider("claude")
test_service_provider("openai")
18 changes: 18 additions & 0 deletions test_ai_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from policyengine_api.services.ai_analysis_service import AIAnalysisService

def test_ai_provider(provider):
print(f"\nTesting {provider} API...")
service = AIAnalysisService()
prompt = "What is the capital of France?"
generator = service.trigger_ai_analysis(prompt, provider=provider)

try:
for response in generator():
print(response)
print(f"{provider} test completed successfully.")
except Exception as e:
print(f"Error testing {provider}: {str(e)}")

if __name__ == "__main__":
test_ai_provider("claude")
test_ai_provider("openai")
Loading