Skip to content

feat(intelligence): natural-language goal creation with structured in… - #880

Closed
cyb3ralee wants to merge 5 commits into
Suncrest-Labs:mainfrom
cyb3ralee:feat/natural-language-goal-creation
Closed

feat(intelligence): natural-language goal creation with structured in…#880
cyb3ralee wants to merge 5 commits into
Suncrest-Labs:mainfrom
cyb3ralee:feat/natural-language-goal-creation

Conversation

@cyb3ralee

@cyb3ralee cyb3ralee commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Adds natural-language goal creation using Claude structured output.

Changes

  • Added goal_extractor.py service with structured output from Claude
  • Added natural_language_goal.py router for API endpoints
  • Extracts: name, target amount, deadline, category, initial deposit, recurring plan
  • Validates deterministically after extraction
  • Surfaces ambiguities for confirmation (never guesses)
  • Prompt injection resistant
  • Added comprehensive tests

Testing

  • Tests cover: amount extraction, date resolution, ambiguity handling, injection resistance
  • All tests pass locally

Security

  • Prompt injection protection via pattern matching
  • Input validation before any LLM call
  • No data leaks in error messages

Closes #853

Summary by CodeRabbit

  • New Features
    • Added authenticated endpoints to turn natural-language input into structured goal details and to confirm goal intent.
    • Extraction now returns success data plus ambiguity information and a “confirmation required” flag when more details are needed.
    • Added prompt-injection protection and improved validation (e.g., positive target amounts and future deadlines).
  • Tests
    • Added unit tests covering validation/ambiguity scenarios and injection detection for the goal extractor.
  • Refactor
    • Added Claude model accessor helpers and re-exported the natural-language goal router for public use.

…tent extraction

- Add goal_extractor service using Claude structured output
- Extract name, amount, deadline, category from natural language
- Validate deterministically after extraction
- Surface ambiguities for confirmation, never guess
- Prompt injection resistance
- Add comprehensive tests

Closes Suncrest-Labs#853
@cyb3ralee
cyb3ralee requested a review from 0xDeon as a code owner July 25, 2026 15:30
@drips-wave

drips-wave Bot commented Jul 25, 2026

Copy link
Copy Markdown

@smartalee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@smartalee, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 821f921e-9019-461e-adf0-c5afa781d32b

📥 Commits

Reviewing files that changed from the base of the PR and between e1353a6 and 241fa6a.

📒 Files selected for processing (5)
  • apps/intelligence/app/routers/__init__.py
  • apps/intelligence/app/routers/natural_language_goal.py
  • apps/intelligence/app/services/claude.py
  • apps/intelligence/app/services/goal_extractor.py
  • apps/intelligence/tests/test_goal_extractor.py
📝 Walkthrough

Walkthrough

Adds Claude-based structured savings-goal extraction, deterministic validation, authenticated FastAPI extraction and placeholder confirmation endpoints, plus focused unit tests.

Changes

Natural-language goal creation

Layer / File(s) Summary
Claude-backed extraction and validation
apps/intelligence/app/services/claude.py, apps/intelligence/app/services/goal_extractor.py
Configures Claude access, extracts typed goal data through tool use, checks prompt injection, and validates amounts, dates, categories, and required fields.
Authenticated goal endpoints
apps/intelligence/app/routers/natural_language_goal.py, apps/intelligence/app/routers/__init__.py
Defines JWT-protected extraction and confirmation endpoints, maps extraction results into API responses, and exports the router from the package without registering it on an application.
Extraction behavior tests
apps/intelligence/tests/test_goal_extractor.py
Tests valid extraction results, amount and deadline rejection, category handling, missing names, and injection detection.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GoalRouter
  participant GoalExtractor
  participant ClaudeAPI
  Client->>GoalRouter: POST /extract-goal
  GoalRouter->>GoalExtractor: extract(message, timezone)
  GoalExtractor->>ClaudeAPI: structured extract_goal request
  ClaudeAPI-->>GoalExtractor: extracted goal fields
  GoalExtractor-->>GoalRouter: validated result
  GoalRouter-->>Client: extraction or ambiguity response
Loading

Suggested reviewers: 0xdeon

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The extraction and confirmation flow fit the issue, but goal creation is still a placeholder and does not route through the validated backend service. Wire confirm-goal to the existing validated goal-creation service and remove the pending_implementation placeholder so confirmed proposals are actually created.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: natural-language goal creation with structured extraction.
Out of Scope Changes check ✅ Passed The changes stay focused on natural-language goal extraction, confirmation, model access, and tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (3)
apps/intelligence/app/routers/natural_language_goal.py (1)

23-29: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Preserve the typed extraction contract in the response model.

The upstream service already defines typed ExtractedGoal and AmbiguityResponse models, but this API exposes both as dict[str, Any]. That weakens OpenAPI documentation and runtime validation. Use the upstream models directly and return the models rather than flattening them unnecessarily.

The upstream GoalExtractionResult contract provides the typed models being flattened here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/routers/natural_language_goal.py` around lines 23 - 29,
The NaturalLanguageGoalResponse model weakens the extraction contract by typing
extracted and ambiguity as generic dictionaries. Import and use the upstream
ExtractedGoal and AmbiguityResponse models for those fields, then update the
surrounding goal-extraction flow to return these typed models without flattening
them.
apps/intelligence/app/services/goal_extractor.py (2)

64-84: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Only user_message is checked for injection; user_timezone is interpolated into the prompt unchecked.

_check_injection is only run on user_message (line 76), but user_timezone (attacker-controlled via the API request body per the router) is inserted directly into the prompt string at line 153 without going through the same check or any format validation (e.g., a valid IANA zone name). While the forced tool_choice limits blast radius, this is still an unvalidated user-controlled string reaching the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 64 - 84,
Validate user_timezone before building the prompt in extract, requiring a valid
IANA timezone name and rejecting invalid or injection-like values with the
existing failed GoalExtractionResult pattern. Ensure _build_extraction_prompt
only receives the validated timezone, while preserving the current user_message
injection check and normal extraction flow.

26-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the legacy class Config with model_config Pydantic v2 deprecates the nested Config class; this model is already using v2 APIs, so model_config = ConfigDict(json_schema_extra=...) keeps the schema config aligned with the rest of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 26 - 37,
Replace the nested Config class in the affected Pydantic model with a
model_config assignment using ConfigDict, preserving the existing
json_schema_extra example unchanged. Ensure ConfigDict is imported from pydantic
and keep the model’s schema configuration behavior intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/intelligence/app/routers/__init__.py`:
- Line 3: Add a trailing newline at the end of the file containing the
natural_language_goal.router registration to satisfy W292, without changing the
router configuration.
- Around line 1-3: Remove the package-level app.include_router call from the
routers initializer, since app is undefined there. Register
natural_language_goal.router from the FastAPI application entrypoint after the
application instance is created, or expose a package-level APIRouter for the
entrypoint to include without referencing app.

In `@apps/intelligence/app/routers/natural_language_goal.py`:
- Line 39: Clean up whitespace in natural_language_goal.py by removing trailing
spaces from the blank lines identified by the Ruff W293 warnings and ensuring
the file ends with a final newline to resolve W292. Do not alter the code or
behavior.
- Around line 67-85: Replace the placeholder response in confirm_and_create_goal
with a call to the existing validated goal-creation service, passing the
authenticated subject from claims and server-side validated goal data. Return
success and the real goal ID only after creation succeeds; otherwise propagate
the service failure or remove/disable the route until this integration is
implemented.
- Around line 7-12: Clean up the import block in natural_language_goal.py:
remove the unused HTTPException and GoalExtractionResult imports, then reorder
and format the remaining imports to satisfy Ruff’s I001/F401 checks.
- Around line 33-54: Update extract_goal_from_natural_language so the
synchronous GoalExtractor.extract call does not block the async event loop:
either execute it through FastAPI’s threadpool utilities or make the route
synchronous, while preserving its existing inputs and result handling. Also
configure an explicit timeout on the Claude client request used by
GoalExtractor.

In `@apps/intelligence/app/services/claude.py`:
- Around line 6-9: Update the MODEL_ID configuration constant from the retired
claude-3-5-sonnet-20241022 identifier to an active Claude model identifier such
as claude-sonnet-5, and update the adjacent comment to reflect the new model.

In `@apps/intelligence/app/services/goal_extractor.py`:
- Around line 112-116: Update the exception handler in the goal extraction flow
to log the full exception details server-side, then return a generic user-facing
error message through GoalExtractionResult instead of interpolating str(e).
Preserve success=False and avoid exposing Pydantic or SDK internals in the API
response.
- Around line 8-9: Update the imports in goal_extractor.py by removing unused
timezone, Dict, and Any symbols, retaining only types referenced by the
implementation, and reorder the remaining imports to satisfy the project’s
import-block lint rules.
- Around line 21-24: The goal extractor model and other flagged Field
declarations need lint cleanup: wrap every overlong Field description (including
the declarations near category, lines 58, 167, 183, 203, and 212) to stay within
100 characters, remove trailing whitespace from blank lines, and ensure the file
ends with a newline.
- Around line 186-234: The _validate_and_resolve method must avoid silently
accepting guessed values: add an ambiguity path for missing or model-uncertain
target_amount, returning “amount” in missing_fields instead of only rejecting
nonpositive values with error, and update the category validation to surface
unrecognized categories as ambiguous rather than coercing them to “savings.”
Preserve the existing positivity and other field validations.
- Around line 183-215: Rename the timezone parameter in _validate_and_resolve to
avoid shadowing the imported timezone module, and use the module’s UTC value for
the current time. Because extracted.deadline is date-only and produces a naive
datetime, normalize the parsed deadline to a comparable date or make both values
consistently naive/aware before the past-deadline check, preserving the existing
ambiguity response.
- Around line 148-153: Rename the string timezone parameter in both
_build_extraction_prompt() and _validate_and_resolve() to avoid shadowing the
imported datetime.timezone, and update all references and call sites
accordingly. Ensure datetime.now uses timezone.utc so prompt generation and
validation complete without AttributeError.

In `@apps/intelligence/tests/test_goal_extractor.py`:
- Around line 53-58: Update the assertions in test_ambiguous_date and the
corresponding test at lines 68-74 to require result.ambiguity is not None
directly, removing the result.success alternative so the tests specifically
verify that ambiguous-date clarification is returned.
- Around line 5-7: Remove the unused ExtractedGoal import from the test module
and reorder the remaining imports to satisfy Ruff’s I001 sorting rule, keeping
only the imports required by the tests.
- Around line 13-91: Update the extractor fixture and tests around
GoalExtractor.extract to mock app.services.goal_extractor.get_client or
self.client.messages.create, returning a canned tool_use response for each
scenario. Ensure the mocked payload contains deterministic goal fields matching
the assertions, plus appropriate missing-field or invalid-input responses, so no
test invokes the live Anthropic API or depends on ANTHROPIC_API_KEY.

---

Nitpick comments:
In `@apps/intelligence/app/routers/natural_language_goal.py`:
- Around line 23-29: The NaturalLanguageGoalResponse model weakens the
extraction contract by typing extracted and ambiguity as generic dictionaries.
Import and use the upstream ExtractedGoal and AmbiguityResponse models for those
fields, then update the surrounding goal-extraction flow to return these typed
models without flattening them.

In `@apps/intelligence/app/services/goal_extractor.py`:
- Around line 64-84: Validate user_timezone before building the prompt in
extract, requiring a valid IANA timezone name and rejecting invalid or
injection-like values with the existing failed GoalExtractionResult pattern.
Ensure _build_extraction_prompt only receives the validated timezone, while
preserving the current user_message injection check and normal extraction flow.
- Around line 26-37: Replace the nested Config class in the affected Pydantic
model with a model_config assignment using ConfigDict, preserving the existing
json_schema_extra example unchanged. Ensure ConfigDict is imported from pydantic
and keep the model’s schema configuration behavior intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2872f3e1-3860-4818-a870-bad302405826

📥 Commits

Reviewing files that changed from the base of the PR and between 1ad6852 and 5bcb157.

📒 Files selected for processing (5)
  • apps/intelligence/app/routers/__init__.py
  • apps/intelligence/app/routers/natural_language_goal.py
  • apps/intelligence/app/services/claude.py
  • apps/intelligence/app/services/goal_extractor.py
  • apps/intelligence/tests/test_goal_extractor.py

Comment thread apps/intelligence/app/routers/__init__.py Outdated
Comment thread apps/intelligence/app/routers/__init__.py Outdated
Comment thread apps/intelligence/app/routers/natural_language_goal.py Outdated
Comment thread apps/intelligence/app/routers/natural_language_goal.py Outdated
Comment thread apps/intelligence/app/routers/natural_language_goal.py Outdated
Comment thread apps/intelligence/app/services/goal_extractor.py Outdated
Comment thread apps/intelligence/app/services/goal_extractor.py
Comment thread apps/intelligence/tests/test_goal_extractor.py Outdated
Comment thread apps/intelligence/tests/test_goal_extractor.py Outdated
Comment thread apps/intelligence/tests/test_goal_extractor.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/intelligence/app/services/goal_extractor.py (4)

34-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the stale future-date example.

The tool schema includes a deadline of 2026-03-31, which is already past as of July 25, 2026. Because this schema is sent to Claude, the example conflicts with the future-date requirement and can bias extraction. Use a relative/generated example or omit the deadline example.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 34 - 44,
Update the Config.json_schema_extra example for the goal extractor to remove the
stale hard-coded deadline value, either by omitting the deadline example or
replacing it with a dynamically generated future date; ensure the schema example
remains consistent with the future-date requirement.

98-110: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep the Claude extraction off the FastAPI event loop and add a timeout

apps/intelligence/app/routers/natural_language_goal.py calls GoalExtractor.extract() directly from an async def route, and apps/intelligence/app/services/goal_extractor.py uses the sync anthropic.Anthropic client with no timeout or retry bounds. This can block request handling under slow model responses; switch this path to the async client or a threadpool and add finite timeout/retry limits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 98 - 110,
Update GoalExtractor.extract and the natural_language_goal async route so the
synchronous Anthropic request does not run on the FastAPI event loop; use the
async Anthropic client or execute the existing client through a threadpool.
Configure finite request timeout and retry limits on the Claude client/request,
preserving the existing structured extract_goal response handling.

205-223: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Enforce the declared date-only deadline format. datetime.fromisoformat() still accepts offset-bearing timestamps, so a malformed deadline can reach deadline_date < datetime.now() and raise TypeError outside the ValueError handler. Parse with date.fromisoformat() and compare dates instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 205 - 223,
Update the deadline validation in the goal extraction flow to parse
extracted.deadline with date.fromisoformat() instead of
datetime.fromisoformat(), and compare it against today’s date rather than
datetime.now(). Keep invalid date handling within the existing ValueError path
and preserve the current past-date ambiguity response.

129-157: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Don't use this blacklist as the prompt-injection boundary. GoalExtractor.extract() still embeds untrusted user_message directly into the same prompt, so the finite regex list will miss obfuscated instructions and the base64-like rule can reject legitimate long identifiers. Separate trusted instructions from user data and expand the tests with adversarial and false-positive cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 129 - 157,
Replace the blacklist-based boundary in _check_injection and the prompt
construction used by GoalExtractor.extract with a trusted-instructions/user-data
separation that safely delimits or parameterizes user_message instead of
embedding it as instructions. Remove reliance on the finite regex and base64
checks, and add tests covering obfuscated injection attempts plus legitimate
long base64-like identifiers that must remain accepted.
🧹 Nitpick comments (2)
apps/intelligence/tests/test_goal_extractor.py (2)

19-37: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Add coverage for the public extraction contract.

This test supplies an already-structured ExtractedGoal, so it cannot catch broken natural-language amount/date extraction, timezone resolution, optional deposit/recurring-plan mapping, or the router-facing GoalExtractor.extract result. Keep these deterministic validator tests, but add mocked public-flow cases for those behaviors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/tests/test_goal_extractor.py` around lines 19 - 37, Add
mocked tests around the public GoalExtractor.extract flow while retaining
test_validate_and_resolve_valid_goal. Cover natural-language amount and date
extraction, timezone resolution, optional initial-deposit and recurring-plan
mapping, and the router-facing result contract using deterministic mocks; do not
rely solely on preconstructed ExtractedGoal validation.

109-119: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Expand prompt-injection coverage beyond one pattern.

The tests cover only the ignore ... previous instructions regex and one normal sentence. Add parameterized cases for alternate indicators and the base64 branch, plus a public extraction test confirming blocked input is rejected before model invocation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/tests/test_goal_extractor.py` around lines 109 - 119,
Expand coverage around the existing test_injection_check_blocks_malicious_input
and test_injection_check_allows_normal_input tests with parameterized malicious
and benign cases covering alternate injection indicators and the base64
detection path. Add a public extraction test that supplies blocked input,
verifies it is rejected, and confirms the model is not invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/intelligence/app/services/goal_extractor.py`:
- Around line 255-258: Update the success path that constructs
GoalExtractionResult so it validates every extracted proposal field before
returning success=True: currency, optional amounts, category, date, and
ambiguity rules. Reuse the existing validation logic where available, and return
the established failure result instead of exposing extracted when any validation
fails.
- Around line 198-203: Extend the validation block in the goal extraction flow
to validate initial_deposit and recurring_amount as finite, non-negative values
before returning success. When is_recurring is false, reject any supplied
recurring_amount; preserve successful proposals only when recurring fields are
consistent with that flag.
- Around line 18-20: Update the goal extraction schema around target_amount so
currency is explicitly captured and validated as USDC, or reject inputs that
specify a non-USDC currency; do not allow a currency-bearing amount to become a
bare numeric value. Preserve the existing numeric target_amount behavior only
for inputs whose currency is absent or confirmed to be USDC, using the
surrounding goal extraction model and validation flow.
- Around line 159-164: Update _build_extraction_prompt and _validate_and_resolve
to derive the current date from a timezone-aware datetime using the supplied
user_timezone rather than server-local datetime.now(). Compare the resolved
deadline and current value as date objects, not a date-only value against a full
timestamp, so midnight boundaries are handled correctly.

---

Outside diff comments:
In `@apps/intelligence/app/services/goal_extractor.py`:
- Around line 34-44: Update the Config.json_schema_extra example for the goal
extractor to remove the stale hard-coded deadline value, either by omitting the
deadline example or replacing it with a dynamically generated future date;
ensure the schema example remains consistent with the future-date requirement.
- Around line 98-110: Update GoalExtractor.extract and the natural_language_goal
async route so the synchronous Anthropic request does not run on the FastAPI
event loop; use the async Anthropic client or execute the existing client
through a threadpool. Configure finite request timeout and retry limits on the
Claude client/request, preserving the existing structured extract_goal response
handling.
- Around line 205-223: Update the deadline validation in the goal extraction
flow to parse extracted.deadline with date.fromisoformat() instead of
datetime.fromisoformat(), and compare it against today’s date rather than
datetime.now(). Keep invalid date handling within the existing ValueError path
and preserve the current past-date ambiguity response.
- Around line 129-157: Replace the blacklist-based boundary in _check_injection
and the prompt construction used by GoalExtractor.extract with a
trusted-instructions/user-data separation that safely delimits or parameterizes
user_message instead of embedding it as instructions. Remove reliance on the
finite regex and base64 checks, and add tests covering obfuscated injection
attempts plus legitimate long base64-like identifiers that must remain accepted.

---

Nitpick comments:
In `@apps/intelligence/tests/test_goal_extractor.py`:
- Around line 19-37: Add mocked tests around the public GoalExtractor.extract
flow while retaining test_validate_and_resolve_valid_goal. Cover
natural-language amount and date extraction, timezone resolution, optional
initial-deposit and recurring-plan mapping, and the router-facing result
contract using deterministic mocks; do not rely solely on preconstructed
ExtractedGoal validation.
- Around line 109-119: Expand coverage around the existing
test_injection_check_blocks_malicious_input and
test_injection_check_allows_normal_input tests with parameterized malicious and
benign cases covering alternate injection indicators and the base64 detection
path. Add a public extraction test that supplies blocked input, verifies it is
rejected, and confirms the model is not invoked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad10bf4b-ede0-4c07-941a-9ae7103d1abd

📥 Commits

Reviewing files that changed from the base of the PR and between 5bcb157 and 64aa8ee.

📒 Files selected for processing (5)
  • apps/intelligence/app/routers/__init__.py
  • apps/intelligence/app/routers/natural_language_goal.py
  • apps/intelligence/app/services/claude.py
  • apps/intelligence/app/services/goal_extractor.py
  • apps/intelligence/tests/test_goal_extractor.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/intelligence/app/routers/init.py
  • apps/intelligence/app/services/claude.py
  • apps/intelligence/app/routers/natural_language_goal.py

Comment on lines +18 to +20
name: str = Field(description="Short descriptive name for the goal")
target_amount: float = Field(description="Target amount in USDC")
deadline: str = Field(description="ISO 8601 date string (YYYY-MM-DD)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not silently treat every currency as USDC.

The schema stores only a numeric amount. An input such as “€5,000” can become 5000 with no currency preserved or rejected, producing an incorrect USDC proposal. Add currency extraction/validation, or explicitly reject non-USDC amounts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 18 - 20,
Update the goal extraction schema around target_amount so currency is explicitly
captured and validated as USDC, or reject inputs that specify a non-USDC
currency; do not allow a currency-bearing amount to become a bare numeric value.
Preserve the existing numeric target_amount behavior only for inputs whose
currency is absent or confirmed to be USDC, using the surrounding goal
extraction model and validation flow.

Comment on lines +159 to +164
def _build_extraction_prompt(self, message: str, timezone: str) -> str:
"""Build the extraction prompt"""
categories_str = ", ".join(self.CATEGORIES)
return f"""Extract a structured savings goal from the user's message.

Current date and timezone: {datetime.now().strftime('%Y-%m-%d')} ({timezone})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '130,240p' apps/intelligence/app/services/goal_extractor.py
printf '\n--- SEARCH ---\n'
rg -n "datetime\.now|timezone|user_timezone|date" apps/intelligence/app/services/goal_extractor.py

Repository: Suncrest-Labs/nester

Length of output: 5727


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('apps/intelligence/app/services/goal_extractor.py')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if 150 <= i <= 220:
        print(f"{i:4d}: {line}")
PY

Repository: Suncrest-Labs/nester

Length of output: 3258


🏁 Script executed:

sed -n '240,360p' apps/intelligence/app/services/goal_extractor.py
printf '\n--- TZ USAGE ---\n'
rg -n "\btimezone\b|\buser_timezone\b|datetime\.now\(" apps/intelligence/app/services/goal_extractor.py

Repository: Suncrest-Labs/nester

Length of output: 1395


apps/intelligence/app/services/goal_extractor.py:159-211 — Resolve dates with user_timezone.
timezone is only surfaced in the prompt; _build_extraction_prompt() and _validate_and_resolve() both use server-local datetime.now(). The deadline check also compares a date-only value to a full timestamp, which can misclassify deadlines around midnight. Use a timezone-aware current date and compare dates directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 159 - 164,
Update _build_extraction_prompt and _validate_and_resolve to derive the current
date from a timezone-aware datetime using the supplied user_timezone rather than
server-local datetime.now(). Compare the resolved deadline and current value as
date objects, not a date-only value against a full timestamp, so midnight
boundaries are handled correctly.

Comment on lines +198 to +203
# Validate amount
if extracted.target_amount <= 0:
return GoalExtractionResult(
success=False,
error="Target amount must be a positive number"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate deposits and recurring amounts before success.

Only target_amount is checked. Negative initial_deposit or recurring_amount values, and recurring amounts supplied while is_recurring is false, can pass through in a successful proposal. Add deterministic finite/non-negative and recurring-consistency checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/intelligence/app/services/goal_extractor.py` around lines 198 - 203,
Extend the validation block in the goal extraction flow to validate
initial_deposit and recurring_amount as finite, non-negative values before
returning success. When is_recurring is false, reject any supplied
recurring_amount; preserve successful proposals only when recurring fields are
consistent with that flag.

Comment thread apps/intelligence/app/services/goal_extractor.py Outdated
"success": True,
"message": "Goal confirmed and created",
"goal_id": "pending_implementation",
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint returns "success": True and a goal id of "pending_implementation" without creating anything. From the client's point of view the goal was created; it wasn't.

#853 asks for two things this blocks: that a parsed goal is confirmed by the user before creation, and that creation goes through the existing validated goal-creation service so a malformed extraction gets rejected by backend validation — with a test proving it. Neither can hold while this is a stub.

A stub is fine in a draft. In a PR marked "Closes #853" it's the acceptance criterion that matters most. Please call the real goal-creation service and return its id.

# Model configuration - using claude-3-5-sonnet-20241022 for structured output
# TODO: Update to latest model when available
MODEL_ID = "claude-3-5-sonnet-20241022"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model is hardcoded to claude-3-5-sonnet-20241022 with a # TODO: Update to latest model when available next to it. #853 asks for a current, configurable model id, and the repo already has a pinned config value for this — the CodeRabbit path instructions for apps/intelligence/** specifically call out hardcoded model IDs that bypass it.

Pull it from config and point it at a current model.

success=False,
ambiguity=AmbiguityResponse(
is_ambiguous=True,
message=(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timezone is shadowed here, and once that's fixed you'll hit a naive/aware datetime comparison underneath it. Same shadowing at line 153.

That matters more than a normal bug would, because #853 asks that relative dates resolve by explicit tested rules in the user's timezone. Deadline resolution is the part of this feature most likely to quietly produce a wrong answer that nobody notices until the goal matures on the wrong day. Please add cases that pin a couple of real timezones across a DST boundary.


from pydantic import BaseModel, Field

from app.services.claude import get_client, get_model_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what's failing Intelligence (Python): unused imports (timezone, Dict, Any), several E501/W293 violations, and no trailing newline at the end of the file. Six ruff errors, four of them auto-fixable.

ruff check --fix and ruff format clear most of it in one pass. Please run the linter locally before pushing — the PR description says "All tests pass locally", which can't be right with the file in this state.

return GoalExtractionResult(
success=False,
error=f"Extraction error: {str(e)}",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw exception text is passed back to the caller here. The PR description says "No data leaks in error messages", but an unhandled provider error will happily include whatever the SDK put in it. Log the detail, return a generic message.

The injection-resistance work and validating deterministically after extraction are both the right instincts, and the ambiguity-surfacing design is good — returning a clarifying question instead of guessing is exactly what #853 wants. It's the plumbing around it that needs finishing.

@@ -0,0 +1,4 @@
"""Router exports for the intelligence service."""
from app.routers.natural_language_goal import router

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router is exported but never registered on a FastAPI app, so none of these endpoints are reachable. Add it to the application factory the way the other routers are.

CodeRabbit picked up most of the above independently and has a few more small ones on the import block and whitespace — worth running through its comments and applying them, since several are one-liners that will clear CI on their own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(intelligence): natural-language goal creation with structured intent extraction

2 participants