diff --git a/.env.example b/.env.example index 1c6ef9ce3..c8db5cf94 100644 --- a/.env.example +++ b/.env.example @@ -13,9 +13,6 @@ POLICYENGINE_DB_INSTANCE_CONNECTION_NAME=policyengine-api:us-central1:policyengi # Github Microdata Token POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN=policyengine_github_token -# API key for Anthropic -ANTHROPIC_API_KEY=policyengine_anthropic_api_key - # API Key for OpenAI OPENAI_API_KEY=policyengine_openai_api_key diff --git a/.github/scripts/cloud_run_env.sh b/.github/scripts/cloud_run_env.sh index 80cc7361d..396d4babe 100755 --- a/.github/scripts/cloud_run_env.sh +++ b/.github/scripts/cloud_run_env.sh @@ -38,7 +38,6 @@ cloud_run_set_defaults() { CLOUD_RUN_STARTUP_PROBE="${CLOUD_RUN_STARTUP_PROBE:-httpGet.path=/readiness-check,httpGet.port=${CLOUD_RUN_PORT},initialDelaySeconds=240,periodSeconds=10,failureThreshold=24,timeoutSeconds=5}" CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET="${CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET:-policyengine-api-prod-db-password:latest}" CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET="${CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET:-policyengine-api-prod-github-microdata-token:latest}" - CLOUD_RUN_ANTHROPIC_API_KEY_SECRET="${CLOUD_RUN_ANTHROPIC_API_KEY_SECRET:-policyengine-api-prod-anthropic-api-key:latest}" CLOUD_RUN_OPENAI_API_KEY_SECRET="${CLOUD_RUN_OPENAI_API_KEY_SECRET:-policyengine-api-prod-openai-api-key:latest}" CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET="${CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET:-policyengine-api-prod-hugging-face-token:latest}" CLOUD_RUN_RUNTIME_CACHE_URL_SECRET="${CLOUD_RUN_RUNTIME_CACHE_URL_SECRET:-policyengine-api-prod-runtime-cache-url:latest}" @@ -75,7 +74,6 @@ cloud_run_set_defaults() { export CLOUD_RUN_STARTUP_PROBE export CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET export CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET - export CLOUD_RUN_ANTHROPIC_API_KEY_SECRET export CLOUD_RUN_OPENAI_API_KEY_SECRET export CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET export CLOUD_RUN_RUNTIME_CACHE_URL_SECRET diff --git a/.github/scripts/deploy_cloud_run_candidate.sh b/.github/scripts/deploy_cloud_run_candidate.sh index 6baa5ed65..261048103 100755 --- a/.github/scripts/deploy_cloud_run_candidate.sh +++ b/.github/scripts/deploy_cloud_run_candidate.sh @@ -41,7 +41,6 @@ fi secret_vars=( "POLICYENGINE_DB_PASSWORD=${CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET}" "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN=${CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET}" - "ANTHROPIC_API_KEY=${CLOUD_RUN_ANTHROPIC_API_KEY_SECRET}" "OPENAI_API_KEY=${CLOUD_RUN_OPENAI_API_KEY_SECRET}" "HUGGING_FACE_TOKEN=${CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET}" "RUNTIME_CACHE_URL=${CLOUD_RUN_RUNTIME_CACHE_URL_SECRET}" diff --git a/.github/scripts/sync_cloud_run_secrets.sh b/.github/scripts/sync_cloud_run_secrets.sh index 75f3325e6..a83b3599c 100644 --- a/.github/scripts/sync_cloud_run_secrets.sh +++ b/.github/scripts/sync_cloud_run_secrets.sh @@ -47,6 +47,5 @@ require_env CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT sync_secret POLICYENGINE_DB_PASSWORD policyengine-api-prod-db-password sync_secret POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN policyengine-api-prod-github-microdata-token -sync_secret ANTHROPIC_API_KEY policyengine-api-prod-anthropic-api-key sync_secret OPENAI_API_KEY policyengine-api-prod-openai-api-key sync_secret HUGGING_FACE_TOKEN policyengine-api-prod-hugging-face-token diff --git a/.github/scripts/validate_app_engine_deploy_env.sh b/.github/scripts/validate_app_engine_deploy_env.sh index 41ca1f6e4..2a7d9d920 100644 --- a/.github/scripts/validate_app_engine_deploy_env.sh +++ b/.github/scripts/validate_app_engine_deploy_env.sh @@ -13,7 +13,6 @@ required=( POLICYENGINE_DB_INSTANCE_CONNECTION_NAME POLICYENGINE_DB_PASSWORD_SECRET_RESOURCE POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE - ANTHROPIC_API_KEY_SECRET_RESOURCE OPENAI_API_KEY_SECRET_RESOURCE HUGGING_FACE_TOKEN_SECRET_RESOURCE SIM_ENTRYPOINT diff --git a/.github/scripts/validate_cloud_run_deploy_env.sh b/.github/scripts/validate_cloud_run_deploy_env.sh index 772c733cb..700cfd490 100755 --- a/.github/scripts/validate_cloud_run_deploy_env.sh +++ b/.github/scripts/validate_cloud_run_deploy_env.sh @@ -26,7 +26,6 @@ cloud_run_require_env \ POLICYENGINE_DB_INSTANCE_CONNECTION_NAME \ CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET \ CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET \ - CLOUD_RUN_ANTHROPIC_API_KEY_SECRET \ CLOUD_RUN_OPENAI_API_KEY_SECRET \ CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET \ CLOUD_RUN_RUNTIME_CACHE_URL_SECRET \ diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fd2f50891..bd91d0b4a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -158,7 +158,6 @@ jobs: env: POLICYENGINE_DB_PASSWORD: ${{ secrets.POLICYENGINE_DB_PASSWORD }} POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: ${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - name: Upload coverage to Codecov diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 7a02df9e0..d320bf972 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -171,7 +171,6 @@ jobs: APP_ENGINE_SERVICE_ACCOUNT: policyengine-api-ae-staging@policyengine-api.iam.gserviceaccount.com POLICYENGINE_DB_PASSWORD_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-db-password/versions/latest POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-github-microdata-token/versions/latest - ANTHROPIC_API_KEY_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-anthropic-api-key/versions/latest OPENAI_API_KEY_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-openai-api-key/versions/latest HUGGING_FACE_TOKEN_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-hugging-face-token/versions/latest GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} @@ -224,7 +223,6 @@ jobs: RUNTIME_CACHE_SERVICE: api POLICYENGINE_DB_PASSWORD: ${{ secrets.POLICYENGINE_DB_PASSWORD }} POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: ${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - name: Validate App Engine deployment configuration @@ -508,7 +506,6 @@ jobs: APP_ENGINE_SERVICE_ACCOUNT: policyengine-api-ae-prod@policyengine-api.iam.gserviceaccount.com POLICYENGINE_DB_PASSWORD_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-db-password/versions/latest POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-github-microdata-token/versions/latest - ANTHROPIC_API_KEY_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-anthropic-api-key/versions/latest OPENAI_API_KEY_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-openai-api-key/versions/latest HUGGING_FACE_TOKEN_SECRET_RESOURCE: projects/policyengine-api/secrets/policyengine-api-prod-hugging-face-token/versions/latest GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} diff --git a/.github/workflows/sync-cloud-run-secrets.yml b/.github/workflows/sync-cloud-run-secrets.yml index 9a6be0717..77b766777 100644 --- a/.github/workflows/sync-cloud-run-secrets.yml +++ b/.github/workflows/sync-cloud-run-secrets.yml @@ -35,7 +35,6 @@ jobs: CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT: ${{ secrets.GCP_CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT }} POLICYENGINE_DB_PASSWORD: ${{ secrets.POLICYENGINE_DB_PASSWORD }} POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: ${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} run: bash .github/scripts/sync_cloud_run_secrets.sh diff --git a/README.md b/README.md index 04386d112..5c4b0918f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ make setup-env - `POLICYENGINE_DB_PASSWORD` - `POLICYENGINE_DB_INSTANCE_CONNECTION_NAME` - `POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN` -- `ANTHROPIC_API_KEY` - `OPENAI_API_KEY` - `HUGGING_FACE_TOKEN` diff --git a/changelog.d/remove-ai-analysis.removed.md b/changelog.d/remove-ai-analysis.removed.md new file mode 100644 index 000000000..1b3f927c3 --- /dev/null +++ b/changelog.d/remove-ai-analysis.removed.md @@ -0,0 +1 @@ +Removed the AI analysis feature (simulation-analysis, tracer-analysis and ai-prompts endpoints, the Anthropic client and its deployment secrets); nothing live called it and it will be rebuilt during the Axiom migration. diff --git a/docs/engineering/migration-contracts.md b/docs/engineering/migration-contracts.md index ca76dabb6..648f79d4d 100644 --- a/docs/engineering/migration-contracts.md +++ b/docs/engineering/migration-contracts.md @@ -6,7 +6,7 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi | Metric | Count | | --- | ---: | -| route group count | 12 | +| route group count | 9 | | workflow count | 7 | | request count | 14 | | db entity count | 6 | @@ -25,9 +25,6 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi | `simulation` | `simulation`, `simulations` | `simulation` | `economy` | | `report` | `report` | `report` | `report` | | `user_profile` | `user-profile` | `user` | `none` | -| `simulation_analysis` | `simulation-analysis` | `none` | `none` | -| `tracer_analysis` | `tracer-analysis` | `none` | `none` | -| `ai` | `ai-prompts` | `none` | `none` | ## App V2 Workflow Contracts diff --git a/docs/engineering/skills/testing.md b/docs/engineering/skills/testing.md index 4beac5b5e..ead193d80 100644 --- a/docs/engineering/skills/testing.md +++ b/docs/engineering/skills/testing.md @@ -80,7 +80,7 @@ lifecycle whenever either Alembic domain changes. Shared-cache unit behavior and real Redis-compatible integration semantics: ```bash -uv run pytest tests/unit/runtime_cache tests/unit/services/test_household_calculation_service.py tests/unit/services/test_tracer_service.py tests/unit/services/test_ai_analysis_service.py tests/unit/services/test_reform_impacts_service.py tests/unit/services/test_budget_window_cache.py -q +uv run pytest tests/unit/runtime_cache tests/unit/services/test_household_calculation_service.py tests/unit/services/test_reform_impacts_service.py tests/unit/services/test_budget_window_cache.py -q RUNTIME_CACHE_TEST_URL="redis://127.0.0.1:6379/0" uv run pytest tests/integration/test_runtime_cache_redis.py -q ``` diff --git a/docs/generated/migration_contracts.json b/docs/generated/migration_contracts.json index 68736b33c..3ae30a010 100644 --- a/docs/generated/migration_contracts.json +++ b/docs/generated/migration_contracts.json @@ -2,7 +2,7 @@ "metadata": { "db_entity_count": 6, "request_count": 14, - "route_group_count": 12, + "route_group_count": 9, "sim_flow_count": 3, "workflow_count": 7 }, @@ -86,30 +86,6 @@ "user-profile" ], "sim_flow": null - }, - { - "db_entity": null, - "name": "simulation_analysis", - "path_segments": [ - "simulation-analysis" - ], - "sim_flow": null - }, - { - "db_entity": null, - "name": "tracer_analysis", - "path_segments": [ - "tracer-analysis" - ], - "sim_flow": null - }, - { - "db_entity": null, - "name": "ai", - "path_segments": [ - "ai-prompts" - ], - "sim_flow": null } ], "version": 1, diff --git a/gcp/README.md b/gcp/README.md index 678bc7749..34bc48c58 100644 --- a/gcp/README.md +++ b/gcp/README.md @@ -12,9 +12,9 @@ Secret Manager wiring. Required runtime settings are `RUNTIME_CACHE_MODE`, The App Engine image is environment-neutral. `app.yaml` receives non-secret configuration and Secret Manager resource names only. Before Gunicorn starts, `policyengine_api.app_engine_runtime` resolves the database password, GitHub -microdata token, Anthropic key, OpenAI key, and Hugging Face token into the -process environment using the attached App Engine service account. Raw values -and temporary secret files must never enter the build context or image layers. +microdata token, OpenAI key, and Hugging Face token into the process environment +using the attached App Engine service account. Raw values and temporary secret +files must never enter the build context or image layers. Local development uses an explicitly launched Redis-compatible process and an explicit durable development database. For example: diff --git a/gcp/export.py b/gcp/export.py index b061be11a..d575c3a11 100644 --- a/gcp/export.py +++ b/gcp/export.py @@ -46,9 +46,6 @@ def _render_app_config() -> str: ".policyengine_github_microdata_auth_token_secret_resource": _required( "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE" ), - ".anthropic_api_key_secret_resource": _required( - "ANTHROPIC_API_KEY_SECRET_RESOURCE" - ), ".openai_api_key_secret_resource": _required("OPENAI_API_KEY_SECRET_RESOURCE"), ".hugging_face_token_secret_resource": _required( "HUGGING_FACE_TOKEN_SECRET_RESOURCE" diff --git a/gcp/policyengine_api/app.yaml b/gcp/policyengine_api/app.yaml index 0a3dc0251..64574a0bf 100644 --- a/gcp/policyengine_api/app.yaml +++ b/gcp/policyengine_api/app.yaml @@ -26,7 +26,6 @@ env_variables: POLICYENGINE_DB_INSTANCE_CONNECTION_NAME: ".policyengine_db_instance_connection_name" POLICYENGINE_DB_PASSWORD_SECRET_RESOURCE: ".policyengine_db_password_secret_resource" POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE: ".policyengine_github_microdata_auth_token_secret_resource" - ANTHROPIC_API_KEY_SECRET_RESOURCE: ".anthropic_api_key_secret_resource" OPENAI_API_KEY_SECRET_RESOURCE: ".openai_api_key_secret_resource" HUGGING_FACE_TOKEN_SECRET_RESOURCE: ".hugging_face_token_secret_resource" SIMULATION_ENTRYPOINT_URL: ".simulation_entrypoint_url" diff --git a/policyengine_api/ai_prompts/__init__.py b/policyengine_api/ai_prompts/__init__.py deleted file mode 100644 index 81c870681..000000000 --- a/policyengine_api/ai_prompts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .simulation_analysis_prompt import generate_simulation_analysis_prompt diff --git a/policyengine_api/ai_prompts/simulation_analysis_prompt.py b/policyengine_api/ai_prompts/simulation_analysis_prompt.py deleted file mode 100644 index 77bb19dae..000000000 --- a/policyengine_api/ai_prompts/simulation_analysis_prompt.py +++ /dev/null @@ -1,116 +0,0 @@ -from policyengine_api.ai_templates.simulation_analysis_template import ( - simulation_analysis_template, - audience_descriptions, -) -from pydantic import BaseModel -from typing import Any, Annotated -import json - - -class InboundParameters(BaseModel): - currency: Annotated[str, "The currency symbol to be used (e.g., $, £)"] - country_id: str - dataset: str | None - selected_version: str - time_period: str - impact: dict[str, dict[str, Any] | str | None] - policy_label: str - policy: dict[str, Any] - region: str - relevant_parameter_baseline_values: list[dict[str, Any]] - relevant_parameters: list[dict[str, Any]] - audience: str - - -class AllParameters(InboundParameters): - model_config = {"exclude": {"audience"}} - dialect: str - data_source: str - poverty_measure: str - poverty_rate_change_text: str - poverty_by_race_text: str - audience_description: str - country_id_uppercase: Annotated[str, "Uppercase two-letter country ID"] - impact_budget: Annotated[str, "JSON deserialized to string"] - impact_intra_decile: Annotated[str, "JSON deserialized to string"] - impact_decile: Annotated[str, "JSON deserialized to string"] - impact_inequality: Annotated[str, "JSON deserialized to string"] - impact_poverty: Annotated[str, "JSON deserialized to string"] - impact_deep_poverty: Annotated[str, "JSON deserialized to string"] - impact_poverty_by_gender: Annotated[str, "JSON deserialized to string"] - - -def generate_simulation_analysis_prompt(params: InboundParameters) -> str: - """ - Generate AI prompt for economy-wide simulations - """ - - parameters: InboundParameters = InboundParameters.model_validate(params) - - dialect: str = "British" if parameters.region == "uk" else "American" - - data_source: str = ( - "certified PolicyEngine UK bundle dataset" - if parameters.region == "uk" - else "certified PolicyEngine US bundle dataset" - ) - - poverty_measure: str = ( - "absolute poverty before housing costs" - if parameters.region == "uk" - else "the Supplemental Poverty Measure" - ) - - poverty_rate_change_text: str = ( - "- After the racial breakdown of poverty rate changes, include the text: '{{povertyImpact.regular.byRace}}'" - if parameters.region == "us" - else "" - ) - - poverty_by_race: str = ( - json.dumps(parameters.impact["poverty_by_race"]["poverty"]) - if parameters.country_id == "us" - else "" - ) - poverty_by_race_text: str = ( - "- This JSON describes the baseline and reform poverty impacts by racial group (briefly " - "describe the relative changes): " + str(poverty_by_race) - if parameters.country_id == "us" - else "" - ) - - audience_description: str = audience_descriptions[parameters.audience] - - country_id_uppercase: Annotated[str, "Uppercase two-letter country ID"] = ( - parameters.country_id.upper() - ) - - impact_budget: str = json.dumps(parameters.impact["budget"]) - impact_intra_decile: dict[str, Any] = json.dumps(parameters.impact["intra_decile"]) - impact_decile: str = json.dumps(parameters.impact["decile"]) - impact_inequality: str = json.dumps(parameters.impact["inequality"]) - impact_poverty: str = json.dumps(parameters.impact["poverty"]["poverty"]) - impact_deep_poverty: str = json.dumps(parameters.impact["poverty"]["deep_poverty"]) - impact_poverty_by_gender: str = json.dumps(parameters.impact["poverty_by_gender"]) - - all_parameters: AllParameters = AllParameters.model_validate( - { - **parameters.dict(), - "dialect": dialect, - "data_source": data_source, - "poverty_measure": poverty_measure, - "poverty_rate_change_text": poverty_rate_change_text, - "poverty_by_race_text": poverty_by_race_text, - "audience_description": audience_description, - "country_id_uppercase": country_id_uppercase, - "impact_budget": impact_budget, - "impact_intra_decile": impact_intra_decile, - "impact_decile": impact_decile, - "impact_inequality": impact_inequality, - "impact_poverty": impact_poverty, - "impact_deep_poverty": impact_deep_poverty, - "impact_poverty_by_gender": impact_poverty_by_gender, - } - ) - - return simulation_analysis_template.format_map(all_parameters.dict()) diff --git a/policyengine_api/ai_templates/simulation_analysis_template.py b/policyengine_api/ai_templates/simulation_analysis_template.py deleted file mode 100644 index dd1de342a..000000000 --- a/policyengine_api/ai_templates/simulation_analysis_template.py +++ /dev/null @@ -1,84 +0,0 @@ -simulation_analysis_template = """ - I'm using PolicyEngine, a free, open source tool to compute the impact of - public policy. I'm writing up an economic analysis of a hypothetical tax-benefit - policy reform. Please write the analysis for me using the details below, in - their order. You should: - - - First explain each provision of the reform, noting that it's hypothetical and - won't represents policy reforms for {time_period} and {region}. Explain how - the parameters are changing from the baseline to the reform values using the given data. - - - Round large numbers like: {currency}3.1 billion, {currency}300 million, - {currency}106,000, {currency}1.50 (never {currency}1.5). - - - Round percentages to one decimal place. - - - Avoid normative language like 'requires', 'should', 'must', and use quantitative statements - over general adjectives and adverbs. If you don't know what something is, don't make it up. - - - Avoid speculating about the intent of the policy or inferring any motives; only describe the - observable effects and impacts of the policy. Refrain from using subjective language or making - assumptions about the recipients and their needs. - - - Use the active voice where possible; for example, write phrases where the reform is the subject, - such as "the reform [or a description of the reform] reduces poverty by x%". - - - Use {dialect} English spelling and grammar. - - - Cite PolicyEngine {country_id_uppercase} v{selected_version} and the {data_source} microdata - when describing policy impacts. - - - When describing poverty impacts, note that the poverty measure reported is {poverty_measure} - - - Don't use headers, but do use Markdown formatting. Use - for bullets, and include a newline after each bullet. - - - Include the following embeds inline, without a header so it flows. - - - Immediately after you describe the changes by decile, include the text: '{{{{distributionalImpact.incomeDecile.relative}}}}' - - - And after the poverty rate changes, include the text: '{{{{povertyImpact.regular.byAge}}}}' - - {poverty_rate_change_text} - - - And after the inequality changes, include the text: "{{{{inequalityImpact}}}}" - - - Make sure to accurately represent the changes observed in the data. - - - This JSON snippet describes the default parameter values: {relevant_parameter_baseline_values} - - - This JSON snippet describes the baseline and reform policies being compared: {policy} - - - {policy_label} has the following impacts from the PolicyEngine microsimulation model: - - - This JSON snippet describes the relevant parameters with more details: {relevant_parameters} - - - This JSON describes the total budgetary impact, the change to tax revenues and benefit - spending (ignore 'households' and 'baseline_net_income': {impact_budget}) - - - This JSON describes how common different outcomes were at each income decile: {impact_intra_decile} - - - This JSON describes the average and relative changes to income by each income decile: {impact_decile} - - - This JSON describes the baseline and reform poverty rates by age group - (describe the relative changes): {impact_poverty} - - - This JSON describes the baseline and reform deep poverty rates by age group - (describe the relative changes): {impact_deep_poverty} - - - This JSON describes the baseline and reform poverty and deep poverty rates - by gender (briefly describe the relative changes): {impact_poverty_by_gender} - - {poverty_by_race_text} - - - This JSON describes three inequality metrics in the baseline and reform, the Gini - coefficient of income inequality, the share of income held by the top 10% of households - and the share held by the top 1% (describe the relative changes): {impact_inequality} - - {audience_description} -""" - -audience_descriptions = { - "ELI5": "Write this for a layperson who doesn't know much about economics or policy. Explain fundamental concepts like taxes, poverty rates, and inequality as needed.", - "Normal": "Write this for a policy analyst who knows a bit about economics and policy.", - "Wonk": "Write this for a policy analyst who knows a lot about economics and policy. Use acronyms and jargon if it makes the content more concise and informative.", -} diff --git a/policyengine_api/api.py b/policyengine_api/api.py index bffaf7eac..8c470596b 100644 --- a/policyengine_api/api.py +++ b/policyengine_api/api.py @@ -42,31 +42,20 @@ def log_timing(message): from policyengine_api.routes.household_routes import household_bp log_timing("Household routes import completed") -from policyengine_api.routes.simulation_analysis_routes import ( - simulation_analysis_bp, -) - -log_timing("Simulation analysis routes import completed") from policyengine_api.routes.policy_routes import policy_bp log_timing("Policy routes import completed") -from policyengine_api.routes.tracer_analysis_routes import tracer_analysis_bp - -log_timing("Tracer analysis routes import completed") from policyengine_api.routes.metadata_routes import metadata_bp log_timing("Metadata routes import completed") from policyengine_api.routes.user_profile_routes import user_profile_bp log_timing("User profile routes import completed") -from policyengine_api.routes.ai_prompt_routes import ai_prompt_bp from policyengine_api.routes.simulation_routes import simulation_bp from policyengine_api.routes.report_output_routes import report_output_bp from policyengine_api.routes.reform_impact_routes import reform_impact_bp from policyengine_api.routes.system_routes import system_bp -log_timing("Base AI routes import completed") - log_timing("Initialising API...") app = application = flask.Flask(__name__) @@ -136,22 +125,12 @@ def log_timing(message): app.register_blueprint(economy_bp) log_timing("Economy routes registered") -# Routes for AI analysis of economy microsim runs -app.register_blueprint(simulation_analysis_bp) -log_timing("Simulation analysis routes registered") - app.register_blueprint(user_profile_bp) log_timing("User profile routes registered") app.register_blueprint(reform_impact_bp) log_timing("Simulations endpoint registered") -app.register_blueprint(tracer_analysis_bp) -log_timing("Tracer analysis routes registered") - -app.register_blueprint(ai_prompt_bp) -log_timing("AI prompt routes registered") - app.register_blueprint(simulation_bp) app.register_blueprint(report_output_bp) diff --git a/policyengine_api/app_engine_runtime.py b/policyengine_api/app_engine_runtime.py index 27328691c..623470ccf 100644 --- a/policyengine_api/app_engine_runtime.py +++ b/policyengine_api/app_engine_runtime.py @@ -15,7 +15,6 @@ "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN", "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN_SECRET_RESOURCE", ), - ("ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_SECRET_RESOURCE"), ("OPENAI_API_KEY", "OPENAI_API_KEY_SECRET_RESOURCE"), ("HUGGING_FACE_TOKEN", "HUGGING_FACE_TOKEN_SECRET_RESOURCE"), ) diff --git a/policyengine_api/migration_registry.py b/policyengine_api/migration_registry.py index 4d672a5e0..bc5171400 100644 --- a/policyengine_api/migration_registry.py +++ b/policyengine_api/migration_registry.py @@ -66,18 +66,6 @@ class RouteGroupConfig: path_segments=("user-profile",), db_entity="user", ), - RouteGroupConfig( - name="simulation_analysis", - path_segments=("simulation-analysis",), - ), - RouteGroupConfig( - name="tracer_analysis", - path_segments=("tracer-analysis",), - ), - RouteGroupConfig( - name="ai", - path_segments=("ai-prompts",), - ), ) ROUTE_GROUP_BY_SEGMENT = { diff --git a/policyengine_api/openapi_spec.yaml b/policyengine_api/openapi_spec.yaml index 3c68f55be..9d0504188 100644 --- a/policyengine_api/openapi_spec.yaml +++ b/policyengine_api/openapi_spec.yaml @@ -972,87 +972,6 @@ paths: type: string message: type: string - /{country_id}/analysis: - post: - summary: Get or trigger policy analysis - operationId: get_analysis - description: Trigger policy analysis for a given prompt or retrieve analysis for a given prompt ID. Specify the country ID. - parameters: - - name: country_id - in: path - description: The country ID. - required: true - schema: - type: string - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - prompt: - type: string - responses: - 200: - description: Analysis or analysis status. - content: - application/json: - schema: - type: object - properties: - status: - type: string - message: - type: string - nullable: true - result: - type: object - properties: - prompt_id: - type: integer - analysis: - type: string - /{country_id}/analysis/{prompt_id}: - get: - summary: Get policy analysis for particular prompt ID - operationId: get_analysis - description: Retrieve analysis for a particular prompt ID, specified in query parameters. - parameters: - - name: country_id - in: path - description: The country ID. - required: true - schema: - type: string - - name: prompt_id - in: path - description: The prompt ID. - required: true - responses: - 200: - description: Analysis with provided prompt ID - content: - application/json: - schema: - type: object - properties: - status: - type: string - result: - type: object - properties: - prompt_id: - type: integer - analysis: - type: string - 405: - description: Method not allowed; no prompt_id provided - content: - text/html: - schema: - type: string - /liveness-check: get: summary: Test for server liveness. diff --git a/policyengine_api/routes/ai_prompt_routes.py b/policyengine_api/routes/ai_prompt_routes.py deleted file mode 100644 index c497613a0..000000000 --- a/policyengine_api/routes/ai_prompt_routes.py +++ /dev/null @@ -1,53 +0,0 @@ -from flask import Blueprint, Response, request -from copy import deepcopy -from policyengine_api.services.ai_prompt_service import AIPromptService -from policyengine_api.utils.payload_validators import validate_country -from policyengine_api.utils.payload_validators.ai import ( - validate_sim_analysis_payload, -) -from werkzeug.exceptions import NotFound, BadRequest -import json - -ai_prompt_bp = Blueprint("ai_prompt", __name__) -ai_prompt_service = AIPromptService() - - -@ai_prompt_bp.route( - "//ai-prompts/", - methods=["POST"], -) -@validate_country -def generate_ai_prompt(country_id, prompt_name: str) -> Response: - """ - Get an AI prompt with a given name, filled with the given data. - """ - print(f"Got request for AI prompt {prompt_name}") - - payload = request.json - - is_payload_valid, message = validate_sim_analysis_payload(payload) - if not is_payload_valid: - raise BadRequest(f"Invalid JSON data; details: {message}") - - input_data = { - **deepcopy(payload), - "country_id": country_id, - } - - prompt: str | None = ai_prompt_service.get_prompt( - name=prompt_name, input_data=input_data - ) - if prompt is None: - raise NotFound(f"Prompt with name {prompt_name} not found.") - - return Response( - json.dumps( - { - "status": "ok", - "message": None, - "result": prompt, - } - ), - status=200, - mimetype="application/json", - ) diff --git a/policyengine_api/routes/simulation_analysis_routes.py b/policyengine_api/routes/simulation_analysis_routes.py deleted file mode 100644 index d9ddc0b97..000000000 --- a/policyengine_api/routes/simulation_analysis_routes.py +++ /dev/null @@ -1,77 +0,0 @@ -from flask import Blueprint, request, Response, stream_with_context -from werkzeug.exceptions import BadRequest -from policyengine_api.services.simulation_analysis_service import ( - SimulationAnalysisService, -) -from policyengine_api.utils.payload_validators import ( - validate_country, -) -from policyengine_api.utils.payload_validators.ai import ( - validate_sim_analysis_payload, -) -import json - -simulation_analysis_bp = Blueprint("simulation_analysis", __name__) -simulation_analysis_service = SimulationAnalysisService() - - -@simulation_analysis_bp.route("//simulation-analysis", methods=["POST"]) -@validate_country -def execute_simulation_analysis(country_id): - print("Got POST request for simulation analysis") - - # Pop items from request payload and validate - # where necessary - payload = request.json - - is_payload_valid, message = validate_sim_analysis_payload(payload) - if not is_payload_valid: - raise BadRequest(f"Invalid JSON data; details: {message}") - - currency: str = payload.get("currency") - selected_version: str = payload.get("selected_version") - dataset: str | None = payload.get("dataset") - time_period: str = payload.get("time_period") - impact: dict = payload.get("impact") - policy_label: str = payload.get("policy_label") - policy: dict = payload.get("policy") - region: str = payload.get("region") - relevant_parameters: list[dict] = payload.get("relevant_parameters") - relevant_parameter_baseline_values: list[dict] = payload.get( - "relevant_parameter_baseline_values" - ) - audience = payload.get("audience", "") - - analysis, analysis_type = simulation_analysis_service.execute_analysis( - country_id, - currency, - dataset, - selected_version, - time_period, - impact, - policy_label, - policy, - region, - relevant_parameters, - relevant_parameter_baseline_values, - audience, - ) - - if analysis_type == "static": - return Response( - json.dumps({"status": "ok", "result": analysis, "message": None}), - mimetype="application/json", - ) - - # Create streaming response - response = Response( - stream_with_context(analysis), - status=200, - mimetype="application/x-ndjson", - ) - - # Set header to prevent buffering on Google App Engine deployment - # (see https://cloud.google.com/appengine/docs/flexible/how-requests-are-handled?tab=python#x-accel-buffering) - response.headers["X-Accel-Buffering"] = "no" - - return response diff --git a/policyengine_api/routes/tracer_analysis_routes.py b/policyengine_api/routes/tracer_analysis_routes.py deleted file mode 100644 index 43695d45c..000000000 --- a/policyengine_api/routes/tracer_analysis_routes.py +++ /dev/null @@ -1,56 +0,0 @@ -from flask import Blueprint, request, Response, stream_with_context -from werkzeug.exceptions import BadRequest -from policyengine_api.utils.payload_validators import ( - validate_country, - validate_tracer_analysis_payload, -) -from policyengine_api.services.tracer_analysis_service import ( - TracerAnalysisService, -) -import json - -tracer_analysis_bp = Blueprint("tracer_analysis", __name__) -tracer_analysis_service = TracerAnalysisService() - - -@tracer_analysis_bp.route("//tracer-analysis", methods=["POST"]) -@validate_country -def execute_tracer_analysis(country_id): - payload = request.json - - is_payload_valid, message = validate_tracer_analysis_payload(payload) - if not is_payload_valid: - raise BadRequest(f"Invalid JSON data; details: {message}") - - household_id = payload.get("household_id") - policy_id = payload.get("policy_id") - variable = payload.get("variable") - if not isinstance(variable, str): - raise BadRequest("variable must be a string") - - analysis, analysis_type = tracer_analysis_service.execute_analysis( - country_id, - household_id, - policy_id, - variable, - ) - - if analysis_type == "static": - return Response( - json.dumps({"status": "ok", "result": analysis, "message": None}), - status=200, - mimetype="application/json", - ) - - # Create streaming response - response = Response( - stream_with_context(analysis), - status=200, - mimetype="application/x-ndjson", - ) - - # Set header to prevent buffering on Google App Engine deployment - # (see https://cloud.google.com/appengine/docs/flexible/how-requests-are-handled?tab=python#x-accel-buffering) - response.headers["X-Accel-Buffering"] = "no" - - return response diff --git a/policyengine_api/runtime_cache/ai_analyses.py b/policyengine_api/runtime_cache/ai_analyses.py deleted file mode 100644 index 6e75e5fd2..000000000 --- a/policyengine_api/runtime_cache/ai_analyses.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Recoverable AI-analysis caching.""" - -from dataclasses import asdict, dataclass - -from policyengine_api.runtime_cache.core import ( - CacheBackend, - CacheNamespace, - RecoverableJSONCache, -) - - -AI_ANALYSIS_SCHEMA_VERSION = 1 -AI_ANALYSIS_TTL_SECONDS = 604_800 - - -@dataclass(frozen=True) -class CachedAnalysis: - prompt: str - analysis: str - status: str = "ok" - - -class AIAnalysisCache: - def __init__(self, client: CacheBackend, namespace: CacheNamespace) -> None: - self._cache = RecoverableJSONCache( - client, - namespace, - family="ai-analysis", - schema_version=AI_ANALYSIS_SCHEMA_VERSION, - ttl_seconds=AI_ANALYSIS_TTL_SECONDS, - ) - - @staticmethod - def _inputs(prompt: str, model: str) -> dict[str, str]: - return {"model": model, "prompt": prompt} - - def get(self, prompt: str, *, model: str) -> CachedAnalysis | None: - payload = self._cache.get(self._inputs(prompt, model)) - if not isinstance(payload, dict): - return None - if payload.get("prompt") != prompt or not isinstance( - payload.get("analysis"), str - ): - return None - return CachedAnalysis( - prompt=prompt, - analysis=payload["analysis"], - status=str(payload.get("status", "ok")), - ) - - def set(self, value: CachedAnalysis, *, model: str) -> bool: - return self._cache.set(self._inputs(value.prompt, model), asdict(value)) diff --git a/policyengine_api/services/ai_analysis_service.py b/policyengine_api/services/ai_analysis_service.py deleted file mode 100644 index 315d4998e..000000000 --- a/policyengine_api/services/ai_analysis_service.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -import os -from collections.abc import Generator -import time -from typing import Callable - -import anthropic -from pydantic import BaseModel - -from policyengine_api.runtime_cache.dependencies import get_runtime_cache_context -from policyengine_api.runtime_cache.core import record_cache_event -from policyengine_api.runtime_cache.ai_analyses import ( - AIAnalysisCache, - CachedAnalysis, -) - - -AI_ANALYSIS_MODEL = "claude-sonnet-4-20250514" - - -class StreamEvent(BaseModel): - type: str - - -class TextEvent(StreamEvent): - type: str = "text" - stream: str - - -class ErrorEvent(StreamEvent): - type: str = "error" - error: str - - -class AIAnalysisService: - """AI analysis operations with short, service-owned ORM scopes.""" - - def __init__( - self, - analysis_cache: AIAnalysisCache | None = None, - claude_client_factory: Callable[[], anthropic.Anthropic] | None = None, - ) -> None: - if analysis_cache is None: - context = get_runtime_cache_context() - analysis_cache = AIAnalysisCache(context.client, context.namespace) - self._analysis_cache = analysis_cache - self._claude_client_factory = claude_client_factory - - def get_existing_analysis( - self, - prompt: str, - ) -> CachedAnalysis | None: - return self._analysis_cache.get(prompt, model=AI_ANALYSIS_MODEL) - - def trigger_ai_analysis( - self, - prompt: str, - ) -> Generator[str, None, None]: - claude_client = ( - self._claude_client_factory() - if self._claude_client_factory is not None - else anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) - ) - - def generate(): - recompute_started_at = time.perf_counter() - response_text = "" - with claude_client.messages.stream( - model=AI_ANALYSIS_MODEL, - max_tokens=1500, - temperature=0.0, - system="You are an AI assistant analyzing policy data. Explain policies clearly and factually. Do not provide commentary, opinions, or quotes. Focus only on describing what the policies do and their direct impacts.", - messages=[{"role": "user", "content": prompt}], - ) as stream: - for event in stream: - if event.type == "error": - record_cache_event( - family="ai-analysis", - event="recompute-failed", - started_at=recompute_started_at, - severity="WARNING", - ) - yield ( - json.dumps( - ErrorEvent(error=event.error["type"]).model_dump() - ) - + "\n" - ) - return - if event.type == "text": - response_text += event.text - yield ( - json.dumps(TextEvent(stream=event.text).model_dump()) + "\n" - ) - record_cache_event( - family="ai-analysis", - event="recompute", - started_at=recompute_started_at, - ) - self._analysis_cache.set( - CachedAnalysis( - prompt=prompt, - analysis=response_text, - status="ok", - ), - model=AI_ANALYSIS_MODEL, - ) - - return generate() diff --git a/policyengine_api/services/ai_prompt_service.py b/policyengine_api/services/ai_prompt_service.py deleted file mode 100644 index f4e88bffa..000000000 --- a/policyengine_api/services/ai_prompt_service.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Callable, Any -from policyengine_api.ai_prompts.simulation_analysis_prompt import ( - generate_simulation_analysis_prompt, -) - -AIPrompt = Callable[[dict[str, Any]], str] - -ALL_AI_PROMPTS: dict[str, AIPrompt] = { - "simulation_analysis": generate_simulation_analysis_prompt, -} - - -class AIPromptService: - def get_prompt(self, name: str, input_data: dict) -> str | None: - """ - Get an AI prompt with a given name, filled with the given data. - """ - - if name in ALL_AI_PROMPTS: - return ALL_AI_PROMPTS[name](input_data) - - return None diff --git a/policyengine_api/services/run_sync_utils.py b/policyengine_api/services/run_sync_utils.py deleted file mode 100644 index 56220fe36..000000000 --- a/policyengine_api/services/run_sync_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -from typing import Any - - -def parse_json_field(value: dict[str, Any] | list[Any] | str | None) -> Any: - if value is None or isinstance(value, (dict, list)): - return value - return json.loads(value) - - -def serialize_json_field(value: dict[str, Any] | list[Any] | str | None) -> str | None: - if value is None or isinstance(value, str): - return value - return json.dumps(value) - - -def get_latest_successful_run_id(runs: list[dict]) -> str | None: - for run in runs: - if run["status"] == "complete": - return run["id"] - return None - - -def run_matches_report_result(run: dict, report_output: dict) -> bool: - return ( - run["status"] == report_output["status"] - and run.get("output") == report_output.get("output") - and run.get("error_message") == report_output.get("error_message") - ) - - -def select_display_report_run( - report_output: dict, runs_descending: list[dict] -) -> dict | None: - active_run_id = report_output.get("active_run_id") - if active_run_id is not None: - for run in runs_descending: - if run["id"] == active_run_id: - return run - - if report_output["status"] == "error": - for run in runs_descending: - if run_matches_report_result(run, report_output): - return run - - latest_successful_run_id = report_output.get("latest_successful_run_id") - if latest_successful_run_id is not None: - for run in runs_descending: - if run["id"] == latest_successful_run_id: - return run - - for run in runs_descending: - if run_matches_report_result(run, report_output): - return run - - return runs_descending[0] if runs_descending else None - - -def determine_parent_pointers( - status: str, runs_descending: list[dict] -) -> tuple[str | None, str | None]: - newest_run = runs_descending[0] if runs_descending else None - latest_successful_run_id = get_latest_successful_run_id(runs_descending) - - if status in {"pending", "running"} and newest_run is not None: - return newest_run["id"], latest_successful_run_id - - if status == "complete": - return None, latest_successful_run_id or ( - newest_run["id"] if newest_run is not None else None - ) - - return None, latest_successful_run_id diff --git a/policyengine_api/services/simulation_analysis_service.py b/policyengine_api/services/simulation_analysis_service.py deleted file mode 100644 index bb6382c87..000000000 --- a/policyengine_api/services/simulation_analysis_service.py +++ /dev/null @@ -1,108 +0,0 @@ -from policyengine_api.services.ai_analysis_service import AIAnalysisService -from policyengine_api.services.ai_prompt_service import AIPromptService -from typing import Generator, Literal - -ai_prompt_service = AIPromptService() - - -class SimulationAnalysisService(AIAnalysisService): - """ - Service for generating AI analysis of economy-wide simulation - runs; this is connected with the simulation_analysis route and - analysis database table - """ - - def execute_analysis( - self, - country_id: str, - currency: str, - dataset: str | None, - selected_version: str, - time_period: str, - impact: dict, - policy_label: str, - policy: dict, - region: str, - relevant_parameters: list[dict], - relevant_parameter_baseline_values: list[dict], - audience: str | None, - ) -> tuple[Generator[str, None, None] | str, Literal["streaming", "static"]]: - """ - Execute AI analysis for economy-wide simulation - - Returns a tuple of: - - The AI analysis as either a streaming output (if new) or - a string (if existing in database) - - The return type (either "streaming" or "static") - - """ - - print("Generating prompt for economy-wide simulation analysis") - - # Create prompt based on data - prompt = self._generate_simulation_analysis_prompt( - time_period, - region, - currency, - policy, - impact, - relevant_parameters, - relevant_parameter_baseline_values, - selected_version, - country_id, - policy_label, - audience, - dataset=dataset, - ) - - print("Checking if AI analysis already exists for this prompt") - # If a calculated record exists for this prompt, return it as a - # streaming response - existing_analysis = self.get_existing_analysis(prompt) - if existing_analysis is not None: - return existing_analysis.analysis, "static" - - print("Found no existing AI analysis; triggering new analysis with Claude") - # Otherwise, pass prompt to Claude, then return streaming function - try: - analysis = self.trigger_ai_analysis(prompt) - return analysis, "streaming" - except Exception as e: - raise e - - def _generate_simulation_analysis_prompt( - self, - time_period, - region, - currency, - policy, - impact, - relevant_parameters, - relevant_parameter_baseline_values, - selected_version, - country_id, - policy_label, - audience, - dataset, - ): - prompt_data: dict = { - "time_period": time_period, - "region": region, - "currency": currency, - "policy": policy, - "impact": impact, - "relevant_parameters": relevant_parameters, - "relevant_parameter_baseline_values": relevant_parameter_baseline_values, - "selected_version": selected_version, - "country_id": country_id, - "policy_label": policy_label, - "audience": audience, - "dataset": dataset, - } - - try: - prompt = ai_prompt_service.get_prompt("simulation_analysis", prompt_data) - return prompt - - except Exception as e: - raise e diff --git a/policyengine_api/services/tracer_analysis_service.py b/policyengine_api/services/tracer_analysis_service.py deleted file mode 100644 index 22e605f74..000000000 --- a/policyengine_api/services/tracer_analysis_service.py +++ /dev/null @@ -1,206 +0,0 @@ -from collections.abc import Callable -from policyengine_api.constants import COUNTRY_PACKAGE_VERSIONS, POLICYENGINE_VERSION -from typing import Generator, Literal -import re -import anthropic -from policyengine_api.services.ai_analysis_service import AIAnalysisService -from werkzeug.exceptions import NotFound -from sqlalchemy import select -from sqlalchemy.orm import Session, sessionmaker - -from policyengine_api.data.orm import get_v1_session_factory -from policyengine_api.data.v1_models import Household, Policy -from policyengine_api.runtime_cache.dependencies import get_runtime_cache_context -from policyengine_api.runtime_cache.ai_analyses import AIAnalysisCache -from policyengine_api.runtime_cache.household_traces import ( - HouseholdTraceCache, - HouseholdTraceIdentity, -) - - -class TracerAnalysisService(AIAnalysisService): - def __init__( - self, - primary_session_factory: sessionmaker[Session] | None = None, - household_trace_cache: HouseholdTraceCache | None = None, - analysis_cache: AIAnalysisCache | None = None, - claude_client_factory: Callable[[], anthropic.Anthropic] | None = None, - ) -> None: - context = get_runtime_cache_context() - super().__init__( - analysis_cache=analysis_cache - or AIAnalysisCache(context.client, context.namespace), - claude_client_factory=claude_client_factory, - ) - self._primary_session_factory = primary_session_factory - self._household_trace_cache = household_trace_cache or HouseholdTraceCache( - context.client, - context.namespace, - ) - - @property - def _primary_sessions(self) -> sessionmaker[Session]: - return self._primary_session_factory or get_v1_session_factory() - - def execute_analysis( - self, - country_id: str, - household_id: str, - policy_id: str, - variable: str, - ) -> tuple[Generator[str, None, None] | str, Literal["static", "streaming"]]: - """ - Executes tracer analysis for a variable in a household - - Returns a tuple of: - - The AI analysis as either a streaming output (if new) or a string (if existing in database) - - The return type (either "streaming" or "static") - """ - - api_version = COUNTRY_PACKAGE_VERSIONS[country_id] - - # Retrieve tracer record from table - try: - tracer: list[str] = self.get_tracer( - country_id, - household_id, - policy_id, - api_version, - ) - except Exception as e: - raise e - - # Parse the tracer output for our given variable - try: - tracer_segment: list[str] = self._parse_tracer_output(tracer, variable) - except Exception as e: - print(f"Error parsing tracer output: {str(e)}") - raise e - - # Get the appropriate prompt template based on country - prompt_template = self._get_prompt_template(country_id) - - # Add the parsed tracer output to the prompt - prompt = prompt_template.format( - variable=variable, tracer_segment=tracer_segment - ) - - # If a calculated record exists for this prompt, return it as a string - existing_analysis = self.get_existing_analysis(prompt) - if existing_analysis is not None: - return existing_analysis.analysis, "static" - - # Otherwise, pass prompt to Claude, then return streaming function - try: - analysis: Generator = self.trigger_ai_analysis(prompt) - return analysis, "streaming" - except Exception as e: - print( - f"Error generating AI analysis within tracer analysis service: {str(e)}" - ) - raise e - - def get_tracer( - self, - country_id: str, - household_id: str, - policy_id: str, - api_version: str, - ) -> list: - try: - with self._primary_sessions() as session: - household = session.scalar( - select(Household).where( - Household.id == int(household_id), - Household.country_id == country_id, - ) - ) - policy = session.scalar( - select(Policy).where( - Policy.id == int(policy_id), - Policy.country_id == country_id, - ) - ) - if household is None or policy is None: - raise NotFound("No household simulation tracer found") - cached = self._household_trace_cache.get( - HouseholdTraceIdentity( - country_id=country_id, - household_id=household.id, - policy_id=policy.id, - household_hash=household.household_hash, - policy_hash=policy.policy_hash, - country_package_version=api_version, - policyengine_version=POLICYENGINE_VERSION, - ) - ) - if cached is None or not cached.tracer_output: - raise NotFound("No household simulation tracer found") - - return cached.tracer_output - - except Exception as e: - print(f"Error getting existing tracer analysis: {str(e)}") - raise e - - def _parse_tracer_output(self, tracer_output, target_variable): - result = [] - target_indent = None - capturing = False - - # Input validation - if not isinstance(target_variable, str) or not isinstance(tracer_output, list): - return result - - # Create a regex pattern to match the exact variable name - # This will match the variable name followed by optional whitespace, - # then optional angle brackets with any content, then optional whitespace - pattern = rf"^(\s*)({re.escape(target_variable)})(?!\w)\s*(?:<[^>]*>)?\s*" - - for line in tracer_output: - # Count leading spaces to determine indentation level - indent = len(line) - len(line.strip()) - - # Check if this line matches our target variable - match = re.match(pattern, line) - if match and not capturing: - target_indent = indent - capturing = True - result.append(line) - elif capturing: - # Stop capturing if we encounter a line with less indentation than the target - if indent <= target_indent: - break - # Capture dependencies (lines with greater indentation) - result.append(line) - - return result - - def _get_prompt_template(self, country_id: str) -> str: - """Get the appropriate prompt template with correct currency symbol based on country.""" - - # Determine currency instruction based on country - currency_instructions = { - "uk": "The response will be rendered as markdown, so preface £ with \\.", - "us": "The response will be rendered as markdown, so preface $ with \\.", - "ca": "The response will be rendered as markdown, so preface $ with \\.", - "il": "The response will be rendered as markdown, so preface ₪ with \\.", - "ng": "The response will be rendered as markdown, so preface ₦ with \\.", - } - - currency_note = currency_instructions.get( - country_id, "The response will be rendered as markdown." - ) - - return f"""{anthropic.HUMAN_PROMPT} You are an AI assistant explaining policy calculations. - The user has run a simulation for the variable '{{variable}}'. - Here's the tracer output: - {{tracer_segment}} - - Please explain this result in clear, factual terms. Your explanation should: - 1. Briefly describe what {{variable}} is. - 2. Explain the main factors that led to this result. - 3. Mention any key thresholds or rules that affected the calculation. - 4. If relevant, suggest how changes in input might affect this result. - - Provide only factual explanations of the policy mechanics. Do not include commentary, opinions, quotes, or phrases like "Certainly!" or "Here's an explanation." {currency_note}""" diff --git a/policyengine_api/utils/__init__.py b/policyengine_api/utils/__init__.py index 07a267c64..58a8dbb62 100644 --- a/policyengine_api/utils/__init__.py +++ b/policyengine_api/utils/__init__.py @@ -1,4 +1,4 @@ -from .json import * -from .cache_utils import * -from .singleton import Singleton -from .get_current_law import get_current_law_policy_id +from .json import * # noqa: F403 +from .cache_utils import * # noqa: F403 +from .singleton import Singleton as Singleton +from .get_current_law import get_current_law_policy_id as get_current_law_policy_id diff --git a/policyengine_api/utils/payload_validators/__init__.py b/policyengine_api/utils/payload_validators/__init__.py index 065e55610..1c3418be7 100644 --- a/policyengine_api/utils/payload_validators/__init__.py +++ b/policyengine_api/utils/payload_validators/__init__.py @@ -1,4 +1,7 @@ -from .validate_tracer_analysis_payload import validate_tracer_analysis_payload -from .validate_country import validate_country -from .validate_set_policy_payload import validate_set_policy_payload -from .validate_household_payload import validate_household_payload +from .validate_country import validate_country as validate_country +from .validate_set_policy_payload import ( + validate_set_policy_payload as validate_set_policy_payload, +) +from .validate_household_payload import ( + validate_household_payload as validate_household_payload, +) diff --git a/policyengine_api/utils/payload_validators/ai/__init__.py b/policyengine_api/utils/payload_validators/ai/__init__.py deleted file mode 100644 index 3830def42..000000000 --- a/policyengine_api/utils/payload_validators/ai/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .validate_sim_analysis_payload import validate_sim_analysis_payload diff --git a/policyengine_api/utils/payload_validators/ai/validate_sim_analysis_payload.py b/policyengine_api/utils/payload_validators/ai/validate_sim_analysis_payload.py deleted file mode 100644 index 734c29184..000000000 --- a/policyengine_api/utils/payload_validators/ai/validate_sim_analysis_payload.py +++ /dev/null @@ -1,40 +0,0 @@ -def validate_sim_analysis_payload(payload: dict) -> tuple[bool, str | None]: - # Check if all required keys are present; note - # that the audience key is optional - required_keys = [ - "currency", - "selected_version", - "time_period", - "impact", - "policy_label", - "policy", - "region", - "relevant_parameters", - "relevant_parameter_baseline_values", - ] - str_keys = [ - "currency", - "selected_version", - "time_period", - "policy_label", - "region", - ] - dict_keys = [ - "policy", - "impact", - ] - list_keys = ["relevant_parameters", "relevant_parameter_baseline_values"] - missing_keys = [key for key in required_keys if key not in payload] - if missing_keys: - return False, f"Missing required keys: {missing_keys}" - - # Check if all keys are of the right type - for key, value in payload.items(): - if key in str_keys and not isinstance(value, str): - return False, f"Key '{key}' must be a string" - elif key in dict_keys and not isinstance(value, dict): - return False, f"Key '{key}' must be a dictionary" - elif key in list_keys and not isinstance(value, list): - return False, f"Key '{key}' must be a list" - - return True, None diff --git a/policyengine_api/utils/payload_validators/validate_household_payload.py b/policyengine_api/utils/payload_validators/validate_household_payload.py index c66f15e26..e3d281aad 100644 --- a/policyengine_api/utils/payload_validators/validate_household_payload.py +++ b/policyengine_api/utils/payload_validators/validate_household_payload.py @@ -1,6 +1,3 @@ -import json - - def validate_household_payload(payload): """ Validate the payload for a POST request to set a household's input data. diff --git a/policyengine_api/utils/payload_validators/validate_tracer_analysis_payload.py b/policyengine_api/utils/payload_validators/validate_tracer_analysis_payload.py deleted file mode 100644 index 415e4e8df..000000000 --- a/policyengine_api/utils/payload_validators/validate_tracer_analysis_payload.py +++ /dev/null @@ -1,29 +0,0 @@ -def validate_tracer_analysis_payload(payload: dict): - # Validate payload - if not payload: - return False, "No payload provided" - - required_keys = ["household_id", "policy_id", "variable"] - for key in required_keys: - if key not in payload: - return False, f"Missing required key: {key}" - - # Validate types and formats - household_id = payload["household_id"] - policy_id = payload["policy_id"] - variable = payload["variable"] - - if not isinstance(household_id, (str, int)) or ( - isinstance(household_id, str) and not household_id.isdigit() - ): - return False, "household_id must be a numeric integer or string" - - if not isinstance(policy_id, (str, int)) or ( - isinstance(policy_id, str) and not policy_id.isdigit() - ): - return False, "policy_id must be a numeric integer or string" - - if not isinstance(variable, str): - return False, "variable must be a string" - - return True, None diff --git a/pyproject.toml b/pyproject.toml index 06c77db1d..11f6b6f6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ dependencies = [ "a2wsgi>=1.10,<2", "alembic>=1.14,<2", - "anthropic>=0.97.0,<1", "assertpy", "click>=8,<9", "cloud-sql-python-connector", diff --git a/tests/env_variables/test_environment_variables.py b/tests/env_variables/test_environment_variables.py index e1c5cec4d..64cc2176b 100644 --- a/tests/env_variables/test_environment_variables.py +++ b/tests/env_variables/test_environment_variables.py @@ -5,7 +5,9 @@ HUGGING_FACE_API_URL = "https://huggingface.co/api/whoami-v2" GITHUB_API_URL = "https://api.github.com/user" -do_not_run_in_debug = lambda: os.getenv("FLASK_DEBUG") == "1" + +def do_not_run_in_debug(): + return os.getenv("FLASK_DEBUG") == "1" class TestEnvironmentVariables: diff --git a/tests/fixtures/services/ai_analysis_service.py b/tests/fixtures/services/ai_analysis_service.py deleted file mode 100644 index 95bba3039..000000000 --- a/tests/fixtures/services/ai_analysis_service.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest -from typing import Optional -from unittest.mock import MagicMock, patch - - -# Event class to mimic Anthropic's streaming response events -class MockEvent: - def __init__( - self, - event_type: str, - text: Optional[str] = None, - error: Optional[dict[str, str]] = None, - ): - self.type = event_type - self.text = text - self.error = error - - -@pytest.fixture() -def patch_anthropic(): - """ - Fixture that patches the anthropic module at the root level. - This ensures all imports of anthropic.Anthropic use our mock. - """ - with patch("anthropic.Anthropic") as mock: - yield mock - - -@pytest.fixture -def mock_stream_text_events(patch_anthropic): - """ - Fixture that configures the mock Anthropic client to stream text events. - """ - - def _configure(text_chunks: list[str]): - # Set up mock client - mock_client = MagicMock() - patch_anthropic.return_value = mock_client - - # Set up mock stream - mock_stream = MagicMock() - mock_client.messages.stream.return_value.__enter__.return_value = mock_stream - - # Configure stream to yield text events - events = [MockEvent(event_type="text", text=chunk) for chunk in text_chunks] - mock_stream.__iter__.return_value = events - - return mock_client - - return _configure - - -@pytest.fixture -def mock_stream_error_event(patch_anthropic): - """ - Fixture that configures the mock Anthropic client to stream an error event. - """ - - def _configure(error_type: str): - # Set up mock client - mock_client = MagicMock() - patch_anthropic.return_value = mock_client - - # Set up mock stream - mock_stream = MagicMock() - mock_client.messages.stream.return_value.__enter__.return_value = mock_stream - - # Configure stream to yield an error event - error_event = MockEvent(event_type="error", error={"type": error_type}) - mock_stream.__iter__.return_value = [error_event] - - return mock_client - - return _configure - - -def parse_to_chunks(input: str) -> list[str]: - """ - The AI analysis service returns streaming output in chunks of 5 characters. - Parse any string to that format. - """ - CHAR_LEN = 5 - - return [input[i : i + CHAR_LEN] for i in range(0, len(input), CHAR_LEN)] diff --git a/tests/fixtures/services/tracer_analysis_service.py b/tests/fixtures/services/tracer_analysis_service.py deleted file mode 100644 index b8894720c..000000000 --- a/tests/fixtures/services/tracer_analysis_service.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest -from policyengine_api.services.tracer_analysis_service import ( - TracerAnalysisService, -) -from unittest.mock import patch -from policyengine_api.runtime_cache.ai_analyses import CachedAnalysis - -valid_tracer_output = [ - " snap<2027, (default)> = [6769.799]", - " snap<2027-01, (default)> = [561.117]", - " takes_up_snap_if_eligible<2027-01, (default)> = [ True]", - " snap_normal_allotment<2027-01, (default)> = [561.117]", - " is_snap_eligible<2027-01, (default)> = [ True]", - " meets_snap_net_income_test<2027-01, (default)> = [ True]", - " snap_net_income_fpg_ratio<2027-01, (default)> = [0.]", - " snap_net_income<2027-01, (default)> = [0.]", - " snap_fpg<2027-01, (default)> = [1806.4779]", -] - -invalid_tracer_output = [ - "only_government_benefit <1500>", - " market_income <1000>", -] - -spliced_valid_tracer_output_root_variable = valid_tracer_output[0:] - -spliced_valid_tracer_output_nested_variable = valid_tracer_output[2:3] - -spliced_valid_tracer_output_leaf_variable = valid_tracer_output[8:] - -spliced_valid_tracer_output_for_variable_that_is_substring_of_another = ( - valid_tracer_output[7:8] -) - -empty_tracer = [] - - -@pytest.fixture -def sample_tracer_data(): - return valid_tracer_output - - -@pytest.fixture -def sample_expected_segment(): - return spliced_valid_tracer_output_nested_variable - - -@pytest.fixture -def mock_get_tracer(sample_tracer_data): - with patch.object( - TracerAnalysisService, "get_tracer", return_value=sample_tracer_data - ) as mock: - yield mock - - -@pytest.fixture -def mock_parse_tracer_output(sample_expected_segment): - with patch.object( - TracerAnalysisService, - "_parse_tracer_output", - return_value=sample_expected_segment, - ) as mock: - yield mock - - -@pytest.fixture -def mock_get_existing_analysis(): - with patch.object( - TracerAnalysisService, - "get_existing_analysis", - return_value=CachedAnalysis( - prompt="prompt", - analysis="Existing static analysis", - status="ok", - ), - ) as mock: - yield mock - - -@pytest.fixture -def mock_trigger_ai_analysis(): - def dummy_generator(): - yield "stream chunk 1" - yield "stream chunk 2" - - with patch.object( - TracerAnalysisService, - "trigger_ai_analysis", - return_value=dummy_generator(), - ) as mock: - yield mock diff --git a/tests/fixtures/services/tracer_fixture_service.py b/tests/fixtures/services/tracer_fixture_service.py deleted file mode 100644 index c1c31d251..000000000 --- a/tests/fixtures/services/tracer_fixture_service.py +++ /dev/null @@ -1,83 +0,0 @@ -import pytest -import json -from types import SimpleNamespace - -from policyengine_api.constants import POLICYENGINE_VERSION -from policyengine_api.data.v1_models import Household, Policy -from policyengine_api.runtime_cache.core import CacheNamespace -from policyengine_api.runtime_cache.fake import InMemoryCacheBackend -from policyengine_api.runtime_cache.household_traces import ( - HouseholdTraceCache, - HouseholdTraceIdentity, - HouseholdTraceValue, -) - -valid_tracer = { - "tracer_output": [ - "only_government_benefit <1500>", - " market_income <1000>", - " employment_income <1000>", - " main_employment_income <1000>", - " non_market_income <500>", - " pension_income <500>", - ] -} - -valid_tracer_row = { - "household_id": "71424", - "policy_id": "2", - "country_id": "us", - "api_version": "1.150.0", - "tracer_output": json.dumps(valid_tracer["tracer_output"]), -} - - -@pytest.fixture -def test_tracer_data(orm_session_factory): - with orm_session_factory.begin() as session: - session.add_all( - [ - Household( - id=int(valid_tracer_row["household_id"]), - country_id=valid_tracer_row["country_id"], - label=None, - api_version=valid_tracer_row["api_version"], - household_json={}, - household_hash="household-hash", - ), - Policy( - id=int(valid_tracer_row["policy_id"]), - country_id=valid_tracer_row["country_id"], - label=None, - api_version=valid_tracer_row["api_version"], - policy_json={}, - policy_hash="policy-hash", - ), - ] - ) - cache = HouseholdTraceCache( - InMemoryCacheBackend(), - CacheNamespace("test", "api"), - ) - cache.set( - HouseholdTraceIdentity( - household_id=int(valid_tracer_row["household_id"]), - policy_id=int(valid_tracer_row["policy_id"]), - country_id=valid_tracer_row["country_id"], - household_hash="household-hash", - policy_hash="policy-hash", - country_package_version=valid_tracer_row["api_version"], - policyengine_version=POLICYENGINE_VERSION, - ), - HouseholdTraceValue( - household={}, - tracer_output=json.loads(valid_tracer_row["tracer_output"]), - ), - ) - return SimpleNamespace( - household_id=int(valid_tracer_row["household_id"]), - policy_id=int(valid_tracer_row["policy_id"]), - country_id=valid_tracer_row["country_id"], - api_version=valid_tracer_row["api_version"], - cache=cache, - ) diff --git a/tests/fixtures/simulation_analysis_prompt_fixtures.py b/tests/fixtures/simulation_analysis_prompt_fixtures.py deleted file mode 100644 index ab3d6be74..000000000 --- a/tests/fixtures/simulation_analysis_prompt_fixtures.py +++ /dev/null @@ -1,214 +0,0 @@ -VALID_DATASET_VERSION = "1.1.1" -VALID_MODEL_VERSION = "1.2.3" - -valid_input_us = { - "time_period": "2022", - "region": "us", - "dataset": None, - "currency": "$", - "policy": {"gov.test.parameter": 0.1}, - "impact": { - "poverty_by_race": { - "poverty": { - "HISPANIC": 0.1, - "WHITE": 0.2, - } - }, - "budget": { - "baseline": 0.0, - "reform": 0.1, - }, - "intra_decile": { - "baseline": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "reform": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - }, - "decile": { - "baseline": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "reform": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - }, - "inequality": { - "baseline": 0.1, - "reform": 0.2, - }, - "poverty": { - "poverty": { - "baseline": 0.1, - "reform": 0.2, - }, - "deep_poverty": { - "baseline": 0.1, - "reform": 0.2, - }, - }, - "poverty_by_gender": { - "baseline": 0.1, - "reform": 0.2, - }, - }, - "relevant_parameters": [ - { - "parameter1": 100, - "parameter2": 200, - } - ], - "relevant_parameter_baseline_values": [ - { - "parameter1": 100, - "parameter2": 200, - } - ], - "selected_version": "1.2.3", - "country_id": "us", - "policy_label": "policy_label", - "audience": "Normal", -} - -valid_input_uk = { - "time_period": "2022", - "region": "uk", - "dataset": None, - "currency": "£", - "policy": {"gov.test.parameter": 0.1}, - "impact": { - "budget": { - "baseline": 0.0, - "reform": 0.1, - }, - "intra_decile": { - "baseline": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "reform": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - }, - "decile": { - "baseline": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "reform": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - }, - "inequality": { - "baseline": 0.1, - "reform": 0.2, - }, - "poverty": { - "poverty": { - "baseline": 0.1, - "reform": 0.2, - }, - "deep_poverty": { - "baseline": 0.1, - "reform": 0.2, - }, - }, - "poverty_by_gender": { - "baseline": 0.1, - "reform": 0.2, - }, - }, - "relevant_parameters": [ - { - "parameter1": 100, - "parameter2": 200, - } - ], - "relevant_parameter_baseline_values": [ - { - "parameter1": 100, - "parameter2": 200, - } - ], - "selected_version": "1.2.3", - "country_id": "uk", - "policy_label": "policy_label", - "audience": "Normal", - "dataset_version": VALID_DATASET_VERSION, - "model_version": VALID_MODEL_VERSION, -} - -invalid_data_missing_input_field = { - k: valid_input_us[k] for k in valid_input_us.keys() - {"time_period"} -} diff --git a/tests/snapshots/simulation_analysis_prompt_uk.txt b/tests/snapshots/simulation_analysis_prompt_uk.txt deleted file mode 100644 index f398d9f78..000000000 --- a/tests/snapshots/simulation_analysis_prompt_uk.txt +++ /dev/null @@ -1,77 +0,0 @@ - - I'm using PolicyEngine, a free, open source tool to compute the impact of - public policy. I'm writing up an economic analysis of a hypothetical tax-benefit - policy reform. Please write the analysis for me using the details below, in - their order. You should: - - - First explain each provision of the reform, noting that it's hypothetical and - won't represents policy reforms for 2022 and uk. Explain how - the parameters are changing from the baseline to the reform values using the given data. - - - Round large numbers like: £3.1 billion, £300 million, - £106,000, £1.50 (never £1.5). - - - Round percentages to one decimal place. - - - Avoid normative language like 'requires', 'should', 'must', and use quantitative statements - over general adjectives and adverbs. If you don't know what something is, don't make it up. - - - Avoid speculating about the intent of the policy or inferring any motives; only describe the - observable effects and impacts of the policy. Refrain from using subjective language or making - assumptions about the recipients and their needs. - - - Use the active voice where possible; for example, write phrases where the reform is the subject, - such as "the reform [or a description of the reform] reduces poverty by x%". - - - Use British English spelling and grammar. - - - Cite PolicyEngine UK v1.2.3 and the certified PolicyEngine UK bundle dataset microdata - when describing policy impacts. - - - When describing poverty impacts, note that the poverty measure reported is absolute poverty before housing costs - - - Don't use headers, but do use Markdown formatting. Use - for bullets, and include a newline after each bullet. - - - Include the following embeds inline, without a header so it flows. - - - Immediately after you describe the changes by decile, include the text: '{{distributionalImpact.incomeDecile.relative}}' - - - And after the poverty rate changes, include the text: '{{povertyImpact.regular.byAge}}' - - - - - And after the inequality changes, include the text: "{{inequalityImpact}}" - - - Make sure to accurately represent the changes observed in the data. - - - This JSON snippet describes the default parameter values: [{'parameter1': 100, 'parameter2': 200}] - - - This JSON snippet describes the baseline and reform policies being compared: {'gov.test.parameter': 0.1} - - - policy_label has the following impacts from the PolicyEngine microsimulation model: - - - This JSON snippet describes the relevant parameters with more details: [{'parameter1': 100, 'parameter2': 200}] - - - This JSON describes the total budgetary impact, the change to tax revenues and benefit - spending (ignore 'households' and 'baseline_net_income': {"baseline": 0.0, "reform": 0.1}) - - - This JSON describes how common different outcomes were at each income decile: {"baseline": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}, "reform": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}} - - - This JSON describes the average and relative changes to income by each income decile: {"baseline": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}, "reform": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}} - - - This JSON describes the baseline and reform poverty rates by age group - (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - This JSON describes the baseline and reform deep poverty rates by age group - (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - This JSON describes the baseline and reform poverty and deep poverty rates - by gender (briefly describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - - - This JSON describes three inequality metrics in the baseline and reform, the Gini - coefficient of income inequality, the share of income held by the top 10% of households - and the share held by the top 1% (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - Write this for a policy analyst who knows a bit about economics and policy. diff --git a/tests/snapshots/simulation_analysis_prompt_us.txt b/tests/snapshots/simulation_analysis_prompt_us.txt deleted file mode 100644 index bd0cce8ad..000000000 --- a/tests/snapshots/simulation_analysis_prompt_us.txt +++ /dev/null @@ -1,77 +0,0 @@ - - I'm using PolicyEngine, a free, open source tool to compute the impact of - public policy. I'm writing up an economic analysis of a hypothetical tax-benefit - policy reform. Please write the analysis for me using the details below, in - their order. You should: - - - First explain each provision of the reform, noting that it's hypothetical and - won't represents policy reforms for 2022 and us. Explain how - the parameters are changing from the baseline to the reform values using the given data. - - - Round large numbers like: $3.1 billion, $300 million, - $106,000, $1.50 (never $1.5). - - - Round percentages to one decimal place. - - - Avoid normative language like 'requires', 'should', 'must', and use quantitative statements - over general adjectives and adverbs. If you don't know what something is, don't make it up. - - - Avoid speculating about the intent of the policy or inferring any motives; only describe the - observable effects and impacts of the policy. Refrain from using subjective language or making - assumptions about the recipients and their needs. - - - Use the active voice where possible; for example, write phrases where the reform is the subject, - such as "the reform [or a description of the reform] reduces poverty by x%". - - - Use American English spelling and grammar. - - - Cite PolicyEngine US v1.2.3 and the certified PolicyEngine US bundle dataset microdata - when describing policy impacts. - - - When describing poverty impacts, note that the poverty measure reported is the Supplemental Poverty Measure - - - Don't use headers, but do use Markdown formatting. Use - for bullets, and include a newline after each bullet. - - - Include the following embeds inline, without a header so it flows. - - - Immediately after you describe the changes by decile, include the text: '{{distributionalImpact.incomeDecile.relative}}' - - - And after the poverty rate changes, include the text: '{{povertyImpact.regular.byAge}}' - - - After the racial breakdown of poverty rate changes, include the text: '{{povertyImpact.regular.byRace}}' - - - And after the inequality changes, include the text: "{{inequalityImpact}}" - - - Make sure to accurately represent the changes observed in the data. - - - This JSON snippet describes the default parameter values: [{'parameter1': 100, 'parameter2': 200}] - - - This JSON snippet describes the baseline and reform policies being compared: {'gov.test.parameter': 0.1} - - - policy_label has the following impacts from the PolicyEngine microsimulation model: - - - This JSON snippet describes the relevant parameters with more details: [{'parameter1': 100, 'parameter2': 200}] - - - This JSON describes the total budgetary impact, the change to tax revenues and benefit - spending (ignore 'households' and 'baseline_net_income': {"baseline": 0.0, "reform": 0.1}) - - - This JSON describes how common different outcomes were at each income decile: {"baseline": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}, "reform": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}} - - - This JSON describes the average and relative changes to income by each income decile: {"baseline": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}, "reform": {"1": 0.1, "2": 0.2, "3": 0.3, "4": 0.4, "5": 0.5, "6": 0.6, "7": 0.7, "8": 0.8, "9": 0.9, "10": 1.0}} - - - This JSON describes the baseline and reform poverty rates by age group - (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - This JSON describes the baseline and reform deep poverty rates by age group - (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - This JSON describes the baseline and reform poverty and deep poverty rates - by gender (briefly describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - - This JSON describes the baseline and reform poverty impacts by racial group (briefly describe the relative changes): {"HISPANIC": 0.1, "WHITE": 0.2} - - - This JSON describes three inequality metrics in the baseline and reform, the Gini - coefficient of income inequality, the share of income held by the top 10% of households - and the share held by the top 1% (describe the relative changes): {"baseline": 0.1, "reform": 0.2} - - Write this for a policy analyst who knows a bit about economics and policy. diff --git a/tests/to_refactor/fixtures/simulation_analysis_fixtures.py b/tests/to_refactor/fixtures/simulation_analysis_fixtures.py deleted file mode 100644 index 220bfc4f7..000000000 --- a/tests/to_refactor/fixtures/simulation_analysis_fixtures.py +++ /dev/null @@ -1,85 +0,0 @@ -test_impact = { - "budget": { - "baseline": 0.0, - "reform": 0.1, - "change": 0.2, - }, - "intra_decile": { - "baseline": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "reform": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - }, - "decile": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8, - "9": 0.9, - "10": 1.0, - }, - "poverty": { - "poverty": 0.3, - "deep_poverty": 0.4, - }, - "poverty_by_gender": { - "baseline": { - "male": 0.5, - "female": 0.6, - }, - "reform": { - "male": 0.7, - "female": 0.8, - }, - }, - "poverty_by_race": {"poverty": 0.6}, - "inequality": { - "baseline": 0.7, - "reform": 0.8, - "change": 0.9, - }, -} - -test_json = { - "currency": "USD", - "selected_version": "2023", - "time_period": "2023", - "dataset": None, - "impact": test_impact, - "policy_label": "Test Policy", - "policy": dict(policy_json="policy details"), - "region": "US", - "relevant_parameters": [ - {"param1": 100}, - {"param2": 200}, - ], - "relevant_parameter_baseline_values": [ - {"param1": 100}, - {"param2": 200}, - ], - "audience": "Normal", -} diff --git a/tests/to_refactor/python/test_simulation_analysis_routes.py b/tests/to_refactor/python/test_simulation_analysis_routes.py deleted file mode 100644 index 7d0086a7b..000000000 --- a/tests/to_refactor/python/test_simulation_analysis_routes.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest.mock import patch -from policyengine_api.data.v1_models import Analysis - -from policyengine_api.services.simulation_analysis_service import ( - SimulationAnalysisService, -) -from tests.to_refactor.fixtures.simulation_analysis_fixtures import ( - test_json, - test_impact, -) - -test_service = SimulationAnalysisService() - - -def test_execute_simulation_analysis_existing_analysis(rest_client): - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.get_existing_analysis" - ) as mock_get_existing: - mock_get_existing.return_value = Analysis( - prompt="prompt", - analysis="Existing analysis", - status="ok", - ) - - response = rest_client.post("/us/simulation-analysis", json=test_json) - - assert response.status_code == 200 - assert "Existing analysis" in response.json["result"] - - -def test_execute_simulation_analysis_new_analysis(rest_client): - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.get_existing_analysis" - ) as mock_get_existing: - mock_get_existing.return_value = None - with patch( - "policyengine_api.services.simulation_analysis_service.AIAnalysisService.trigger_ai_analysis" - ) as mock_trigger: - mock_trigger.return_value = (s for s in ["New analysis"]) - - response = rest_client.post("/us/simulation-analysis", json=test_json) - - assert response.status_code == 200 - assert b"New analysis" in response.data - - -def test_execute_simulation_analysis_error(rest_client): - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.get_existing_analysis" - ) as mock_get_existing: - mock_get_existing.return_value = None - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.trigger_ai_analysis" - ) as mock_trigger: - mock_trigger.side_effect = Exception("Test error") - - response = rest_client.post("/us/simulation-analysis", json=test_json) - - assert response.status_code == 500 - assert "Test error" in response.json.get("message") - - -def test_execute_simulation_analysis_custom_dataset(rest_client): - policy_details = dict(policy_json="policy details") - - test_json_custom_dataset = { - "currency": "USD", - "selected_version": "2023", - "time_period": "2023", - "impact": test_impact, - "policy_label": "Test Policy", - "policy": policy_details, - "region": "us", - "dataset": "custom_dataset", - "relevant_parameters": ["param1", "param2"], - "relevant_parameter_baseline_values": [ - {"param1": 100}, - {"param2": 200}, - ], - "audience": "Normal", - } - with patch( - "policyengine_api.services.simulation_analysis_service.SimulationAnalysisService._generate_simulation_analysis_prompt" - ): - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.get_existing_analysis" - ) as mock_get_existing: - mock_get_existing.return_value = None - with patch( - "policyengine_api.services.ai_analysis_service.AIAnalysisService.trigger_ai_analysis" - ) as mock_trigger: - mock_trigger.return_value = (s for s in ["Custom dataset analysis"]) - - response = rest_client.post( - "/us/simulation-analysis", json=test_json_custom_dataset - ) - - assert response.status_code == 200 - assert b"Custom dataset analysis" in response.data diff --git a/tests/to_refactor/python/test_tracer_analysis_routes.py b/tests/to_refactor/python/test_tracer_analysis_routes.py deleted file mode 100644 index 851c67bef..000000000 --- a/tests/to_refactor/python/test_tracer_analysis_routes.py +++ /dev/null @@ -1,201 +0,0 @@ -from flask import json -from unittest.mock import patch -from werkzeug.exceptions import NotFound - -# constants -VALID_HOUSEHOLD_ID = 123 -VALID_POLICY_ID = 456 -INVALID_HOUSEHOLD_ID = "abc123" -INVALID_POLICY_ID = "invalid-id" -TEST_VARIABLE = "disposable_income" -INVALID_VARIABLE = 123 - - -@patch("policyengine_api.routes.tracer_analysis_routes.tracer_analysis_service") -def test_execute_tracer_analysis_success(mock_service, rest_client): - mock_service.execute_analysis.return_value = ( - iter(["AI analysis result"]), - "streaming", - ) - test_household_id = 1500 - - # Set this to US current law - test_policy_id = 2 - - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": test_household_id, - "policy_id": test_policy_id, - "variable": "disposable_income", - }, - ) - - assert response.status_code == 200 - assert b"AI analysis result" in response.data - - -@patch("policyengine_api.routes.tracer_analysis_routes.tracer_analysis_service") -def test_execute_tracer_analysis_no_tracer(mock_service, rest_client): - mock_service.execute_analysis.side_effect = NotFound( - "No household simulation tracer found" - ) - - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": VALID_HOUSEHOLD_ID, - "policy_id": VALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - - assert response.status_code == 404 - assert ( - "No household simulation tracer found" in json.loads(response.data)["message"] - ) - - -@patch("policyengine_api.routes.tracer_analysis_routes.tracer_analysis_service") -def test_execute_tracer_analysis_ai_error(mock_service, rest_client): - mock_service.execute_analysis.side_effect = Exception(KeyError) - - test_household_id = 1500 - test_policy_id = 2 - - # Use the test client to make the request instead of calling the function directly - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": test_household_id, - "policy_id": test_policy_id, - "variable": "disposable_income", - }, - ) - - assert response.status_code == 500 - assert json.loads(response.data)["status"] == "error" - - -def test_invalid_variable_types(rest_client): - """Test that different non-string variable types are rejected""" - invalid_variables = [ - 123, - None, - {"key": "value"}, - ["list"], - True, - ] - - for invalid_var in invalid_variables: - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": VALID_HOUSEHOLD_ID, - "policy_id": VALID_POLICY_ID, - "variable": invalid_var, - }, - ) - assert response.status_code == 400 - assert "variable must be a string" in json.loads(response.data)["message"] - - -# Test invalid country -def test_invalid_country(rest_client): - response = rest_client.post( - "/invalid_country/tracer-analysis", - json={ - "household_id": "test_household", - "policy_id": "test_policy", - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - assert b"Country invalid_country not found" in response.data - - -def test_invalid_household_id_format(rest_client): - """Test that non-numeric household_id is rejected""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": INVALID_HOUSEHOLD_ID, - "policy_id": VALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - assert ( - "household_id must be a numeric integer or string" - in json.loads(response.data)["message"] - ) - - -def test_invalid_policy_id_format(rest_client): - """Test that non-numeric policy_id is rejected""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": VALID_HOUSEHOLD_ID, - "policy_id": INVALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - assert ( - "policy_id must be a numeric integer or string" - in json.loads(response.data)["message"] - ) - - -def test_empty_household_id(rest_client): - """Test that empty household_id is rejected""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": "", - "policy_id": VALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - - -def test_missing_required_fields(rest_client): - """Test that missing required fields are rejected""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - # household_id missing - "policy_id": VALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - - -def test_invalid_types(rest_client): - """Test that invalid types are rejected""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - "household_id": None, # Invalid type - "policy_id": INVALID_POLICY_ID, - "variable": "disposable_income", - }, - ) - assert response.status_code == 400 - - -def test_validate_tracer_analysis_payload_failure(rest_client): - """Test handling of invalid payload from validate_tracer_analysis_payload""" - response = rest_client.post( - "/us/tracer-analysis", - json={ - # Missing required field 'variable' - "household_id": VALID_HOUSEHOLD_ID, - "policy_id": VALID_POLICY_ID, - }, - ) - assert response.status_code == 400 - assert "Missing required key: variable" in json.loads(response.data)["message"] diff --git a/tests/to_refactor/python/test_validate_sim_analysis_payload.py b/tests/to_refactor/python/test_validate_sim_analysis_payload.py deleted file mode 100644 index b95871896..000000000 --- a/tests/to_refactor/python/test_validate_sim_analysis_payload.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest -from typing import Dict, Any, Tuple - -from policyengine_api.utils.payload_validators.ai import ( - validate_sim_analysis_payload, -) - - -@pytest.fixture -def valid_payload() -> Dict[str, Any]: - return { - "currency": "USD", - "selected_version": "v1.0", - "time_period": "2024", - "impact": {"value": 100}, - "policy_label": "Test Policy", - "policy": {"type": "tax", "rate": 0.1}, - "region": "NA", - "relevant_parameters": ["param1", "param2"], - "relevant_parameter_baseline_values": [1.0, 2.0], - } - - -def test_valid_payload(valid_payload): - """Test that a valid payload passes validation""" - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is True - assert error is None - - -def test_missing_required_key(valid_payload): - """Test that missing required keys are detected""" - del valid_payload["currency"] - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is False - assert "Missing required keys: ['currency']" in error - - -def test_invalid_string_type(valid_payload): - """Test that wrong type for string fields is detected""" - valid_payload["currency"] = 123 # Should be string - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is False - assert "Key 'currency' must be a string" in error - - -def test_invalid_dict_type(valid_payload): - """Test that wrong type for dictionary fields is detected""" - valid_payload["impact"] = ["not", "a", "dict"] # Should be dict - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is False - assert "Key 'impact' must be a dictionary" in error - - -def test_invalid_list_type(valid_payload): - """Test that wrong type for list fields is detected""" - valid_payload["relevant_parameters"] = "not a list" # Should be list - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is False - assert "Key 'relevant_parameters' must be a list" in error - - -def test_extra_keys_allowed(valid_payload): - """Test that extra keys don't cause validation to fail""" - valid_payload["extra_key"] = "some value" - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is True - assert error is None - - -@pytest.mark.parametrize( - "key", - [ - "currency", - "selected_version", - "time_period", - "impact", - "policy_label", - "policy", - "region", - "relevant_parameters", - "relevant_parameter_baseline_values", - ], -) -def test_individual_required_keys(valid_payload, key): - """Test that each required key is properly checked""" - del valid_payload[key] - is_valid, error = validate_sim_analysis_payload(valid_payload) - assert is_valid is False - assert f"Missing required keys: ['{key}']" in error diff --git a/tests/unit/ai_prompts/test_simulation_analysis_prompt.py b/tests/unit/ai_prompts/test_simulation_analysis_prompt.py deleted file mode 100644 index 2fe1bcc52..000000000 --- a/tests/unit/ai_prompts/test_simulation_analysis_prompt.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from policyengine_api.ai_prompts.simulation_analysis_prompt import ( - generate_simulation_analysis_prompt, -) -from tests.fixtures.simulation_analysis_prompt_fixtures import ( - valid_input_us, - valid_input_uk, - invalid_data_missing_input_field, -) - - -class TestGenerateSimulationAnalysisPrompt: - def test_given_valid_us_input(self, snapshot): - snapshot.snapshot_dir = "tests/snapshots" - - prompt = generate_simulation_analysis_prompt(valid_input_us) - snapshot.assert_match(prompt, "simulation_analysis_prompt_us.txt") - - def test_given_valid_uk_input(self, snapshot): - snapshot.snapshot_dir = "tests/snapshots" - - prompt = generate_simulation_analysis_prompt(valid_input_uk) - snapshot.assert_match(prompt, "simulation_analysis_prompt_uk.txt") - - def test_given_missing_input_field(self): - with pytest.raises( - Exception, - match="1 validation error for InboundParameters\ntime_period\n Field required", - ): - generate_simulation_analysis_prompt(invalid_data_missing_input_field) diff --git a/tests/unit/data/test_runtime_sql_boundaries.py b/tests/unit/data/test_runtime_sql_boundaries.py index 6f42a5708..7b447aa9d 100644 --- a/tests/unit/data/test_runtime_sql_boundaries.py +++ b/tests/unit/data/test_runtime_sql_boundaries.py @@ -31,9 +31,7 @@ def test_ordinary_runtime_modules_no_longer_use_raw_sql_facade(): "routes/policy_routes.py", "routes/reform_impact_routes.py", "country.py", - "services/ai_analysis_service.py", "services/reform_impacts_service.py", - "services/tracer_analysis_service.py", ) for relative_path in relative_paths: source = (PACKAGE_ROOT / relative_path).read_text(encoding="utf-8") diff --git a/tests/unit/routes/test_decorator_order.py b/tests/unit/routes/test_decorator_order.py index 20a967299..07e3a6fd6 100644 --- a/tests/unit/routes/test_decorator_order.py +++ b/tests/unit/routes/test_decorator_order.py @@ -1,19 +1,13 @@ """Regression tests for issue #3446. -economy_routes and ai_prompt_routes originally stacked -@validate_country above @bp.route. Because Flask only inspects the -function registered by bp.route, the wrapping logic ran in the wrong -order: validate_country bypassed the Response it returned, or Flask -saw a decorator that hadn't been registered as a route handler. -The fix puts @bp.route as the outermost decorator. - -An invalid country must now produce a 400 from validate_country -instead of a 200/500 from the view function. +The economy routes originally stacked @validate_country above @bp.route. +Because Flask only inspects the function registered by bp.route, the wrapping +logic ran in the wrong order. An invalid country must produce a 400 from +validate_country instead of a 200/500 from the view function. """ from flask import Flask -from policyengine_api.routes.ai_prompt_routes import ai_prompt_bp from policyengine_api.routes.economy_routes import economy_bp @@ -29,24 +23,3 @@ def test_economy_route_rejects_bogus_country(): client = _client_with(economy_bp) response = client.get("/bogus/economy/1/over/2?region=us&time_period=2025") assert response.status_code == 400 - - -def test_ai_prompt_route_rejects_bogus_country(): - client = _client_with(ai_prompt_bp) - # Use a payload that passes validate_sim_analysis_payload so the only - # remaining reason to 400 is the unknown country_id. With the pre-#3446 - # decorator order the view runs first and reaches the service, so this - # request would not be rejected on country grounds. - valid_payload = { - "currency": "USD", - "selected_version": "v1.0", - "time_period": "2024", - "impact": {"value": 100}, - "policy_label": "Test Policy", - "policy": {"type": "tax", "rate": 0.1}, - "region": "NA", - "relevant_parameters": ["param1", "param2"], - "relevant_parameter_baseline_values": [1.0, 2.0], - } - response = client.post("/bogus/ai-prompts/some_prompt", json=valid_payload) - assert response.status_code == 400 diff --git a/tests/unit/runtime_cache/test_ai_analyses.py b/tests/unit/runtime_cache/test_ai_analyses.py deleted file mode 100644 index 288fdfd40..000000000 --- a/tests/unit/runtime_cache/test_ai_analyses.py +++ /dev/null @@ -1,19 +0,0 @@ -"""AI-analysis cache tests.""" - -from policyengine_api.runtime_cache.ai_analyses import ( - AIAnalysisCache, - CachedAnalysis, -) -from policyengine_api.runtime_cache.core import CacheNamespace -from policyengine_api.runtime_cache.fake import InMemoryCacheBackend - - -def test_analysis_cache_is_model_and_prompt_specific_and_expiring() -> None: - backend = InMemoryCacheBackend() - cache = AIAnalysisCache(backend, CacheNamespace("test", "api")) - value = CachedAnalysis(prompt="explain", analysis="answer") - - assert cache.set(value, model="model-a") is True - assert cache.get("explain", model="model-a") == value - assert cache.get("explain", model="model-b") is None - assert cache.get("different", model="model-a") is None diff --git a/tests/unit/services/test_ai_analysis_service.py b/tests/unit/services/test_ai_analysis_service.py deleted file mode 100644 index 6f2fb8de0..000000000 --- a/tests/unit/services/test_ai_analysis_service.py +++ /dev/null @@ -1,117 +0,0 @@ -import json -from types import SimpleNamespace - -import pytest - -from policyengine_api.runtime_cache.core import CacheNamespace -from policyengine_api.runtime_cache.fake import InMemoryCacheBackend -from policyengine_api.runtime_cache.ai_analyses import AIAnalysisCache -from policyengine_api.services.ai_analysis_service import ( - AI_ANALYSIS_MODEL, - AIAnalysisService, -) -from tests.fixtures.services.ai_analysis_service import parse_to_chunks - -pytest_plugins = ["tests.fixtures.services.ai_analysis_service"] - - -def _cache() -> AIAnalysisCache: - return AIAnalysisCache( - InMemoryCacheBackend(), - CacheNamespace("test", "api"), - ) - - -class TestTriggerAIAnalysis: - def test_claude_stream_caches_only_after_successful_completion(self): - cache = _cache() - - class ClaudeStream: - def __enter__(self): - return self - - def __exit__(self, *args): - return None - - def __iter__(self): - assert cache.get("prompt", model=AI_ANALYSIS_MODEL) is None - yield SimpleNamespace(type="text", text="analysis") - - claude_client = SimpleNamespace( - messages=SimpleNamespace(stream=lambda **kwargs: ClaudeStream()) - ) - service = AIAnalysisService( - cache, - claude_client_factory=lambda: claude_client, - ) - - assert list(service.trigger_ai_analysis("prompt")) == [ - json.dumps({"type": "text", "stream": "analysis"}) + "\n" - ] - stored = cache.get("prompt", model=AI_ANALYSIS_MODEL) - assert stored is not None - assert stored.analysis == "analysis" - - def test_trigger_ai_analysis_given_successful_streaming( - self, mock_stream_text_events - ): - # GIVEN a series of successful text messages from the Claude API - expected_response = "This is a historical quote." - text_chunks = parse_to_chunks(expected_response) - mock_stream_text_events(text_chunks=text_chunks) - - # WHEN we call trigger_ai_analysis - prompt = "Tell me a historical quote" - cache = _cache() - generator = AIAnalysisService(cache).trigger_ai_analysis(prompt) - - # THEN it should yield the expected chunks - results = list(generator) - - # Verify each yielded chunk - for i, chunk in enumerate(results): - if i < len(text_chunks): - expected_chunk = ( - json.dumps({"type": "text", "stream": text_chunks[i][:5]}) + "\n" - ) - assert chunk == expected_chunk - - analysis_record = cache.get(prompt, model=AI_ANALYSIS_MODEL) - - assert analysis_record is not None - assert analysis_record.analysis == expected_response - assert analysis_record.status == "ok" - - @pytest.mark.parametrize( - "error_type", - [ - "overloaded_error", - "api_error", - "unknown_error", - ], - ) - def test_trigger_ai_analysis_given_error(self, mock_stream_error_event, error_type): - # GIVEN an overloaded_error event from the Claude API - mock_stream_error_event(error_type) - - # WHEN we call trigger_ai_analysis - prompt = "Tell me a historical quote about erroneous systems" - cache = _cache() - generator = AIAnalysisService(cache).trigger_ai_analysis(prompt) - - # THEN it should yield the expected error message - results = list(generator) - - # Verify the error message - expected_error = ( - json.dumps( - { - "type": "error", - "error": error_type, - } - ) - + "\n" - ) - assert results[0] == expected_error - - assert cache.get(prompt, model=AI_ANALYSIS_MODEL) is None diff --git a/tests/unit/services/test_anthropic_sdk_contract.py b/tests/unit/services/test_anthropic_sdk_contract.py deleted file mode 100644 index 70eec83c0..000000000 --- a/tests/unit/services/test_anthropic_sdk_contract.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Guard the installed anthropic SDK against the call shape the services use. - -The AI analysis services call ``claude_client.messages.stream(...)`` with a -``temperature`` keyword. The unit suites mock the client with permissive -fakes, so an SDK whose real signature dropped that keyword (anthropic 1.x -removed ``temperature``/``top_p``/``top_k`` and ``HUMAN_PROMPT``) still -passes every mocked test while failing at runtime on a cache miss. This test -checks the real installed SDK instead. -""" - -import inspect - -import anthropic -from anthropic.resources.messages import Messages - - -def test_installed_anthropic_sdk_accepts_stream_temperature(): - parameters = inspect.signature(Messages.stream).parameters - assert "temperature" in parameters, ( - f"anthropic {anthropic.__version__} no longer accepts temperature in " - "Messages.stream(); migrate ai_analysis_service before raising the pin" - ) - - -def test_installed_anthropic_sdk_is_pre_1_0(): - major = int(anthropic.__version__.split(".")[0]) - assert major < 1, ( - f"anthropic {anthropic.__version__} installed; pyproject pins <1 until " - "the services are migrated to the 1.x call surface" - ) diff --git a/tests/unit/services/test_direct_orm_local_analysis.py b/tests/unit/services/test_direct_orm_local_analysis.py index ee8612641..a0ef9d91f 100644 --- a/tests/unit/services/test_direct_orm_local_analysis.py +++ b/tests/unit/services/test_direct_orm_local_analysis.py @@ -4,18 +4,10 @@ from policyengine_api.runtime_cache.core import CacheNamespace from policyengine_api.runtime_cache.fake import InMemoryCacheBackend -from policyengine_api.runtime_cache.ai_analyses import ( - AIAnalysisCache, - CachedAnalysis, -) from policyengine_api.runtime_cache.reform_impacts import ( CachedReformImpact, ReformImpactCache, ) -from policyengine_api.services.ai_analysis_service import ( - AI_ANALYSIS_MODEL, - AIAnalysisService, -) from policyengine_api.services.reform_impacts_service import ReformImpactsService @@ -23,20 +15,6 @@ def _context(): return InMemoryCacheBackend(), CacheNamespace("test", "api") -def test_ai_analysis_service_returns_typed_cached_analysis() -> None: - backend, namespace = _context() - cache = AIAnalysisCache(backend, namespace) - cache.set( - CachedAnalysis(prompt="prompt", analysis="new"), - model=AI_ANALYSIS_MODEL, - ) - - analysis = AIAnalysisService(cache).get_existing_analysis("prompt") - - assert isinstance(analysis, CachedAnalysis) - assert analysis.analysis == "new" - - def test_reform_impact_service_writes_typed_expiring_cache_entity() -> None: backend, namespace = _context() impact = ReformImpactsService( diff --git a/tests/unit/services/test_execute_analysis.py b/tests/unit/services/test_execute_analysis.py deleted file mode 100644 index 25a2d7a59..000000000 --- a/tests/unit/services/test_execute_analysis.py +++ /dev/null @@ -1,58 +0,0 @@ -from policyengine_api.services.tracer_analysis_service import ( - TracerAnalysisService, -) - -pytest_plugins = ["tests.fixtures.services.tracer_analysis_service"] - -country_id = "us" -household_id = "71424" -policy_id = "2" -target_variable = "takes_up_snap_if_eligible" - - -class TestExecuteAnalysis: - def test_execute_analysis_static( - self, - mock_get_tracer, - mock_parse_tracer_output, - mock_get_existing_analysis, - ): - """ - GIVEN a valid tracer data and an expected parsed segment (included as fixture), - AND get_existing_analysis returns a static analysis (included as fixture), - WHEN execute_analysis is called, - THEN then a static analysis with the "static" flag should be returned. - """ - - analysis, analysis_type = TracerAnalysisService().execute_analysis( - country_id, household_id, policy_id, target_variable - ) - - assert analysis == "Existing static analysis" - assert analysis_type == "static" - - def test_execute_analysis_streaming( - self, - mock_get_tracer, - mock_parse_tracer_output, - mock_get_existing_analysis, - mock_trigger_ai_analysis, - ): - """ - GIVEN a valid tracer data and an expected parsed segment, - AND get_existing_analysis returns None, - WHEN execute_analysis is called, - THEN trigger_ai_analysis is called and returns a generator with the "streaming" flag. - """ - - # When existing analysis value is None - mock_get_existing_analysis.return_value = None - - analysis, analysis_type = TracerAnalysisService().execute_analysis( - country_id, household_id, policy_id, target_variable - ) - - expected_streaming_output = ["stream chunk 1", "stream chunk 2"] - streaming_output = list(analysis) - assert streaming_output == expected_streaming_output - assert analysis_type == "streaming" diff --git a/tests/unit/services/test_local_data_service_boundaries.py b/tests/unit/services/test_local_data_service_boundaries.py index cdf0ab4f7..be078a9eb 100644 --- a/tests/unit/services/test_local_data_service_boundaries.py +++ b/tests/unit/services/test_local_data_service_boundaries.py @@ -5,12 +5,7 @@ import pytest -from policyengine_api.services.ai_analysis_service import AIAnalysisService from policyengine_api.services.reform_impacts_service import ReformImpactsService -from policyengine_api.services.simulation_analysis_service import ( - SimulationAnalysisService, -) -from policyengine_api.services.tracer_analysis_service import TracerAnalysisService SERVICE_ROOT = Path(__file__).parents[3] / "policyengine_api" / "services" @@ -19,9 +14,7 @@ @pytest.mark.parametrize( "module_name", [ - "ai_analysis_service.py", "reform_impacts_service.py", - "tracer_analysis_service.py", "report_output_alias_service.py", ], ) @@ -35,9 +28,6 @@ def test_local_data_services_do_not_issue_queries_directly(module_name): @pytest.mark.parametrize( ("service_type", "method_names"), [ - (AIAnalysisService, ("get_existing_analysis", "trigger_ai_analysis")), - (SimulationAnalysisService, ("execute_analysis",)), - (TracerAnalysisService, ("execute_analysis", "get_tracer")), ( ReformImpactsService, ( @@ -62,17 +52,6 @@ def test_local_service_public_methods_do_not_accept_persistence( assert "session_factory" not in parameters -def test_analysis_routes_do_not_manage_sessions(): - route_root = SERVICE_ROOT.parent / "routes" - for module_name in ( - "simulation_analysis_routes.py", - "tracer_analysis_routes.py", - ): - source = (route_root / module_name).read_text(encoding="utf-8") - assert "get_v1_session_factory" not in source - assert "sqlalchemy" not in source - - def test_reform_impact_route_does_not_manage_sessions(): source = (SERVICE_ROOT.parent / "routes" / "reform_impact_routes.py").read_text( encoding="utf-8" diff --git a/tests/unit/services/test_service_owned_session_architecture.py b/tests/unit/services/test_service_owned_session_architecture.py index dbf89e8ef..4e412e16d 100644 --- a/tests/unit/services/test_service_owned_session_architecture.py +++ b/tests/unit/services/test_service_owned_session_architecture.py @@ -5,7 +5,6 @@ import pytest -from policyengine_api.services.ai_analysis_service import AIAnalysisService from policyengine_api.services.household_calculation_service import ( HouseholdCalculationService, ) @@ -13,11 +12,7 @@ from policyengine_api.services.policy_service import PolicyService from policyengine_api.services.reform_impacts_service import ReformImpactsService from policyengine_api.services.report_output_service import ReportOutputService -from policyengine_api.services.simulation_analysis_service import ( - SimulationAnalysisService, -) from policyengine_api.services.simulation_service import SimulationService -from policyengine_api.services.tracer_analysis_service import TracerAnalysisService from policyengine_api.services.user_policy_service import UserPolicyService from policyengine_api.services.user_service import UserService @@ -66,9 +61,6 @@ "update_report_output", ), ), - (AIAnalysisService, ("get_existing_analysis", "trigger_ai_analysis")), - (SimulationAnalysisService, ("execute_analysis",)), - (TracerAnalysisService, ("execute_analysis", "get_tracer")), ( ReformImpactsService, ( diff --git a/tests/unit/services/test_tracer_analysis_service.py b/tests/unit/services/test_tracer_analysis_service.py deleted file mode 100644 index 3cd65cf39..000000000 --- a/tests/unit/services/test_tracer_analysis_service.py +++ /dev/null @@ -1,145 +0,0 @@ -import pytest - -from policyengine_api.services.tracer_analysis_service import ( - TracerAnalysisService, -) -import logging - -from tests.fixtures.services.tracer_analysis_service import * - -logger = logging.getLogger(__name__) -test_service = TracerAnalysisService() - - -def test_tracer_output_for_int_input_variable(): - # Given: A tracer output with various income types - # and a target variable that is an integer (invalid format) - invalid_target_variable_incorrect_type = 500 - - # When: Parsing the tracer output with an invalid target variable - result = test_service._parse_tracer_output( - valid_tracer_output, invalid_target_variable_incorrect_type - ) - - # Then: It should return an empty list - expected_output = empty_tracer - assert result == expected_output - - -def test_tracer_output_for_dict_input_tracer(): - # Given: A tracer output with various income types but is in invalid format (dict instead of list of string) - # and a valid target variable present in the tracer - valid_target_variable = "snap" - - # When: Parsing the tracer output with a dictionary as the target variable - result = test_service._parse_tracer_output( - invalid_tracer_output, valid_target_variable - ) - - # Then: It should return an empty list - expected_output = empty_tracer - assert result == expected_output - - -def test_tracer_output_for_garbage_variable(): - # Given: A tracer output with various income types - # and a target variable that is a malformed string - invalid_target_variable_malformed_string = "<1500>" - - # When: Parsing the tracer output with a malformed string - result = test_service._parse_tracer_output( - valid_tracer_output, invalid_target_variable_malformed_string - ) - - # Then: It should return an empty list - expected_output = empty_tracer - assert result == expected_output - - -def test_tracer_output_for_missing_variable(): - # Given: A tracer output with various income types - # and a target variable that does not exist in the tracer output - missing_target_variable = "non_existent_variable" - - # When: Extracting the segment for a missing variable - result = test_service._parse_tracer_output( - valid_tracer_output, missing_target_variable - ) - - # Then: It should return an empty list since the variable is not present in the tracer output - expected_output = empty_tracer - assert result == expected_output - - -def test_tracer_output_for_empty_tracer(): - # Given: An empty tracer output list - # and some target variable - valid_target_variable = "snap" - - # When: Extracting from an empty output - result = test_service._parse_tracer_output(empty_tracer, valid_target_variable) - - # Then: It should return an empty list since there is no data to parse - expected_output = empty_tracer - assert result == expected_output - - -def test_tracer_output_for_root_variable(): - # Given: A tracer output with various income types - # and a root-level variable that serves as a container for multiple sub-variables - valid_target_variable = "snap" - - # When: Extracting the entire segment - result = test_service._parse_tracer_output( - valid_tracer_output, valid_target_variable - ) - - # Then: It should return everything inside input tracer - expected_output = spliced_valid_tracer_output_root_variable - assert result == expected_output - - -def test_tracer_output_for_nested_variable(): - # Given: A tracer output with various income types - # and a deeply nested variable in the hierarchy - valid_target_variable = "takes_up_snap_if_eligible" - - # When: Extracting nested variable - result = test_service._parse_tracer_output( - valid_tracer_output, valid_target_variable - ) - - # Then: It should return segment for nested variable and its children - expected_output = spliced_valid_tracer_output_nested_variable - assert result == expected_output - - -def test_tracer_output_for_leaf_variable(): - # Given: A tracer output with various income types - # and a target variable that has no child variables - valid_target_variable = "snap_fpg" - - # When: Extracting leaf variable from tracer - result = test_service._parse_tracer_output( - valid_tracer_output, valid_target_variable - ) - - # Then: It should return only leaf variable since it has no children - expected_output = spliced_valid_tracer_output_leaf_variable - assert result == expected_output - - -def test_tracer_output_for_variable_that_is_substring_of_another(): - # Given: A tracer output with variables where one is a substring of another - # and the target variable that's a substring of another variable - target_variable = "snap_net_income" - - # When: Extracting the segment for this variable - result = test_service._parse_tracer_output(valid_tracer_output, target_variable) - - # Then: It should return only the exact match for "snap_net_income", not "snap_net_income_fpg_ratio" - - expected_output = ( - spliced_valid_tracer_output_for_variable_that_is_substring_of_another - ) - assert result == expected_output diff --git a/tests/unit/services/test_tracer_service.py b/tests/unit/services/test_tracer_service.py deleted file mode 100644 index 8b5482275..000000000 --- a/tests/unit/services/test_tracer_service.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest -from policyengine_api.services.tracer_analysis_service import ( - TracerAnalysisService, -) -from werkzeug.exceptions import NotFound - -from tests.fixtures.services.tracer_fixture_service import valid_tracer - -pytest_plugins = ["tests.fixtures.services.tracer_fixture_service"] - - -def test_get_tracer_valid( - test_tracer_data, - orm_session_factory, -): - # Test get_tracer successfully retrieves valid data from the database. - - result = TracerAnalysisService( - primary_session_factory=orm_session_factory, - household_trace_cache=test_tracer_data.cache, - ).get_tracer( - test_tracer_data.country_id, - test_tracer_data.household_id, - test_tracer_data.policy_id, - test_tracer_data.api_version, - ) - - # match the valid output as collected from fixture - valid_output = valid_tracer["tracer_output"] - assert result == valid_output - - -def test_get_tracer_not_found(orm_session_factory): - # Test get_tracer raises NotFound when no matching record exists. - valid_country_val_in_db = "us" - invalid_household_not_in_db = "9999999" - invalid_policyID_not_in_db = "999" - invalid_api_version = "9.999.0" - data_not_in_db = [ - valid_country_val_in_db, - invalid_household_not_in_db, - invalid_policyID_not_in_db, - invalid_api_version, - ] - with pytest.raises(NotFound): - TracerAnalysisService(primary_session_factory=orm_session_factory).get_tracer( - *data_not_in_db - ) - - -def test_get_tracer_database_error(orm_session_factory): - # Test get_tracer handles database errors properly. - missing_country_id = "" - valid_householdID = "71424" - valid_policyID = "2" - valid_api_version = "1.150.0" - missing_parameter_causing_database_exception = [ - missing_country_id, - valid_householdID, - valid_policyID, - valid_api_version, - ] - with pytest.raises(Exception): - TracerAnalysisService(primary_session_factory=orm_session_factory).get_tracer( - *missing_parameter_causing_database_exception, - ) diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index 7e4a90d78..6257f0f3f 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -31,14 +31,12 @@ "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN": ( "policyengine-api-prod-github-microdata-token:latest" ), - "ANTHROPIC_API_KEY": "policyengine-api-prod-anthropic-api-key:latest", "OPENAI_API_KEY": "policyengine-api-prod-openai-api-key:latest", "HUGGING_FACE_TOKEN": "policyengine-api-prod-hugging-face-token:latest", } RAW_CLOUD_RUN_SECRET_VALUES = ( "raw-db-secret-value", "raw-github-secret-value", - "raw-anthropic-secret-value", "raw-openai-secret-value", "raw-hf-secret-value", ) @@ -51,10 +49,6 @@ "projects/policyengine-api/secrets/" "policyengine-api-prod-github-microdata-token/versions/latest" ), - "ANTHROPIC_API_KEY_SECRET_RESOURCE": ( - "projects/policyengine-api/secrets/" - "policyengine-api-prod-anthropic-api-key/versions/latest" - ), "OPENAI_API_KEY_SECRET_RESOURCE": ( "projects/policyengine-api/secrets/" "policyengine-api-prod-openai-api-key/versions/latest" @@ -120,7 +114,6 @@ def _required_runtime_env() -> dict[str, str]: "POLICYENGINE_DB_INSTANCE_CONNECTION_NAME": PRODUCTION_CLOUD_SQL_INSTANCE, "POLICYENGINE_DB_PASSWORD": "raw-db-secret-value", "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN": ("raw-github-secret-value"), - "ANTHROPIC_API_KEY": "raw-anthropic-secret-value", "OPENAI_API_KEY": "raw-openai-secret-value", "HUGGING_FACE_TOKEN": "raw-hf-secret-value", "SIMULATION_ENTRYPOINT_URL": "https://simulation.example.test", @@ -831,7 +824,6 @@ def test_app_engine_bundle_contains_runtime_environment_placeholders(): for prohibited in ( "POLICYENGINE_DB_PASSWORD = os.environ", "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN = os.environ", - "ANTHROPIC_API_KEY = os.environ", "OPENAI_API_KEY = os.environ", "HUGGING_FACE_TOKEN = os.environ", 'open(".dbpw"', @@ -1931,7 +1923,6 @@ def test_push_workflow_does_not_pass_raw_secrets_to_cloud_run_deploy_jobs(): "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: " "${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }}" ), - "ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}", "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}", "HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}", ) @@ -1957,7 +1948,6 @@ def test_push_workflow_app_engine_deploys_use_secret_resources_not_values(): "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: " "${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }}" ), - "ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}", "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}", "HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}", ) diff --git a/tests/unit/test_migration_contract_artifacts.py b/tests/unit/test_migration_contract_artifacts.py index 163c2aeb7..014e3f8c1 100644 --- a/tests/unit/test_migration_contract_artifacts.py +++ b/tests/unit/test_migration_contract_artifacts.py @@ -9,7 +9,7 @@ def test_migration_contract_payload_summarizes_route_contracts(): assert payload["version"] == 1 assert payload["metadata"] == { - "route_group_count": 12, + "route_group_count": 9, "workflow_count": 7, "request_count": 14, "db_entity_count": 6, diff --git a/tests/unit/test_migration_flags.py b/tests/unit/test_migration_flags.py index d26fff66c..8368b0462 100644 --- a/tests/unit/test_migration_flags.py +++ b/tests/unit/test_migration_flags.py @@ -125,7 +125,6 @@ def test_invalid_migration_flag_raises(monkeypatch): ("/us/simulation/1", "simulation"), ("/simulations", "simulation"), ("/us/report/1", "report"), - ("/us/ai-prompts/simulation_analysis", "ai"), ], ) def test_infer_route_group(path, expected_group): diff --git a/uv.lock b/uv.lock index e9dacf541..a35e676f7 100644 --- a/uv.lock +++ b/uv.lock @@ -203,25 +203,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "anthropic" -version = "0.97.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/93/f66ea8bfe39f2e6bb9da8e27fa5457ad2520e8f7612dfc547b17fad55c4d/anthropic-0.97.0.tar.gz", hash = "sha256:021e79fd8e21e90ad94dc5ba2bbbd8b1599f424f5b1fab6c06204009cab764be", size = 669502, upload-time = "2026-04-23T20:52:34.445Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/b6/8e851369fa661ad0fef2ae6266bf3b7d52b78ccf011720058f4adaca59e2/anthropic-0.97.0-py3-none-any.whl", hash = "sha256:8a1a472dfabcfc0c52ff6a3eecf724ac7e07107a2f6e2367be55ceb42f5d5613", size = 662126, upload-time = "2026-04-23T20:52:32.377Z" }, -] - [[package]] name = "anyio" version = "4.13.0" @@ -756,15 +737,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] -[[package]] -name = "docstring-parser" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, -] - [[package]] name = "dpath" version = "2.2.0" @@ -2642,12 +2614,11 @@ models = [ [[package]] name = "policyengine-api" -version = "3.49.1" +version = "3.49.3" source = { editable = "." } dependencies = [ { name = "a2wsgi" }, { name = "alembic" }, - { name = "anthropic" }, { name = "assertpy" }, { name = "click" }, { name = "cloud-sql-python-connector" }, @@ -2695,7 +2666,6 @@ dev = [ requires-dist = [ { name = "a2wsgi", specifier = ">=1.10,<2" }, { name = "alembic", specifier = ">=1.14,<2" }, - { name = "anthropic", specifier = ">=0.97.0,<1" }, { name = "assertpy" }, { name = "build", marker = "extra == 'dev'" }, { name = "click", specifier = ">=8,<9" },