Repository navigation
feat: add public no-auth /resolve endpoint - #93
Conversation
Adds a single, stateless, unauthenticated POST /resolve endpoint that runs the full classify -> sentiment -> similarity -> decide -> respond pipeline and returns the result directly, with no ticket record created. - Extracted the core pipeline out of run_ticket_automation into a new resolve_message() function in ticket_service.py, shared by both the ticket-creation flow and the new endpoint (no logic duplicated). - New app/api/public.py + app/schemas/public.py for the public API. - Registered first in main.py / OpenAPI docs since it's the endpoint most integrators need. - Added a friendly GET / landing route and a README quickstart with curl/JS/Python snippets for dropping this into another repo. - New tests/api/test_public.py covering auth-free access, response shape, validation, and that nothing gets persisted. Existing ticket-creation behavior is unchanged; full suite (660 tests) passes.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe PR adds an unauthenticated ChangesPublic resolution API
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant PublicAPI
participant resolve_message
participant SimilarityLookup
Client->>PublicAPI: POST /resolve with message
PublicAPI->>resolve_message: Resolve message
resolve_message->>SimilarityLookup: Search for similar messages
SimilarityLookup-->>resolve_message: Similarity result
resolve_message-->>PublicAPI: Decision and metadata
PublicAPI-->>Client: ResolveResponse
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@app/schemas/public.py`:
- Around line 39-41: Update the description on the response_source field in the
response schema to document the actual pipeline source values: similarity,
openai, template, and fallback; remove the incorrect similar_ticket example.
- Around line 17-23: The ResolveRequest.message field accepts whitespace-only
input; add a Pydantic validator that rejects messages containing no
non-whitespace characters while preserving existing length validation. Update
tests/api/test_public.py lines 51-58 with a matching /resolve test covering
whitespace-only input; update app/schemas/public.py lines 17-23 in
ResolveRequest.
In `@app/services/ticket_service.py`:
- Around line 202-211: Prevent private resolved-ticket content from reaching
unauthenticated public resolution: update the resolver around generate_response
in app/services/ticket_service.py (lines 202-211) to support a public-safe mode
that disables private-ticket similarity or uses a curated public corpus; invoke
that mode from the public route in app/api/public.py (lines 42-61). Add coverage
in tests/api/test_public.py (lines 15-29) by creating a resolved ticket with
unique private text and asserting /resolve neither returns the text nor selects
it as a similarity response.
- Around line 254-257: Update the escalation branch in the ticket status
handling to set ticket.response_source to None alongside ticket.response when
clearing an automated response, keeping both fields synchronized.
🪄 Autofix
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: 50867a02-9156-4b21-811b-ffbcf3e6e547
📒 Files selected for processing (6)
README.mdapp/api/public.pyapp/main.pyapp/schemas/public.pyapp/services/ticket_service.pytests/api/test_public.py
| message: str = Field( | ||
| ..., | ||
| min_length=1, | ||
| max_length=4000, | ||
| description="The customer's question or issue, as plain text.", | ||
| examples=["I was charged twice for my last order, can I get a refund?"], | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -HI -t f '^(pyproject\.toml|requirements.*\.txt|Pipfile|poetry\.lock|uv\.lock)$' . \
-x rg -n -i 'pydantic' {}
rg -n -C 3 'class ResolveRequest|message: str|str_strip_whitespace|field_validator|`@validator`' \
app/schemas/public.pyRepository: Yad4o/SRS
Length of output: 195
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^app/schemas/public\.py$|(^tests/api/test_public\.py$|public\.py$|ticket_service\.py$|settings.*\.(py|toml))$)'
echo "== schema outline =="
ast-grep outline app/schemas/public.py --view expanded || true
echo "== public schema relevant lines =="
cat -n app/schemas/public.py | sed -n '1,80p'
echo "== test public relevant lines =="
cat -n tests/api/test_public.py | sed -n '1,90p'
echo "== route/service references =="
rg -n -C 3 'ResolveRequest|message|resolve|text/plain|classification|sentiment|similarity|strip\(message\)|\bmessage\b' app testsRepository: Yad4o/SRS
Length of output: 50366
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== schema =="
sed -n '1,120p' app/schemas/public.py
echo "== tests api public =="
sed -n '1,90p' tests/api/test_public.py
echo "== public api resolve route =="
sed -n '1,120p' app/api/public.py
echo "== ticket service normalize/call candidates =="
sed -n '160,260p' app/services/ticket_service.py
echo "== exact references =="
rg -n -C 2 'ResolveRequest|json=request|message_to|normalize|strip\(|classification|sentiment|similarity|compare|vector|cosine|ResolveRequest' app tests --glob '*.py'Repository: Yad4o/SRS
Length of output: 50366
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== resolve_message header =="
rg -n -C 12 '^def resolve_message|message_to|classify_intent|analyze_sentiment' app/services/ticket_service.py
echo "== service helper normalize =="
sed -n '80,125p' app/utils/service_helpers.py
echo "== classifier/sentiment empty behavior =="
rg -n -C 5 'def classify_intent|classify_intent_ai|def analyze_sentiment|analyze_sentiment_ai|strip\(\)|normalize\(.*\)|len\(tokens\)|raise ValueError|return' app/services/classifier.py app/services/ai_service.py app/services/ticket_service.py app/utils/service_helpers.py --glob '*.py'Repository: Yad4o/SRS
Length of output: 48996
Reject whitespace-only messages.
min_length=1 accepts " \t", which reaches the AI pipeline after normalization. Add a Pydantic validator on ResolveRequest.message that rejects whitespace-only input and add a matching /resolve test.
📍 Affects 2 files
app/schemas/public.py#L17-L23(this comment)tests/api/test_public.py#L51-L58
🤖 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 `@app/schemas/public.py` around lines 17 - 23, The ResolveRequest.message field
accepts whitespace-only input; add a Pydantic validator that rejects messages
containing no non-whitespace characters while preserving existing length
validation. Update tests/api/test_public.py lines 51-58 with a matching /resolve
test covering whitespace-only input; update app/schemas/public.py lines 17-23 in
ResolveRequest.
| response_source: str | None = Field( | ||
| None, description="Where the response came from, e.g. 'template' or 'similar_ticket'." | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the response_source documentation.
The pipeline returns similarity, not similar_ticket. Document the actual source values: similarity, openai, template, and fallback.
Proposed fix
- None, description="Where the response came from, e.g. 'template' or 'similar_ticket'."
+ None, description="Where the response came from: similarity, openai, template, or fallback."📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| response_source: str | None = Field( | |
| None, description="Where the response came from, e.g. 'template' or 'similar_ticket'." | |
| ) | |
| response_source: str | None = Field( | |
| None, description="Where the response came from: similarity, openai, template, or fallback." | |
| ) |
🤖 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 `@app/schemas/public.py` around lines 39 - 41, Update the description on the
response_source field in the response schema to document the actual pipeline
source values: similarity, openai, template, and fallback; remove the incorrect
similar_ticket example.
| similar_solution = ( | ||
| similar_result["ticket"]["response"] if similar_result else None | ||
| ) | ||
| response_text, response_source = generate_response( | ||
| intent, | ||
| ticket.message, | ||
| message, | ||
| similar_solution=similar_solution, | ||
| sub_intent=sub_intent, | ||
| similar_quality_score=similar_quality_score, | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Prevent unauthenticated cross-ticket response disclosure.
The resolver uses a matching resolved-ticket response as public output material. The public route supplies untrusted callers to this pipeline without authentication. PII sanitization is not an authorization boundary and does not make private ticket content publishable.
app/services/ticket_service.py#L202-L211: Do not use the resolved-ticket corpus for public resolution. Use a curated public knowledge corpus, or add an explicit mode that disables private-ticket similarity.app/api/public.py#L42-L61: Invoke the resolver in its public-safe mode.tests/api/test_public.py#L15-L29: Create a resolved ticket with unique private text and assert that/resolvecannot return that text or select it as a public similarity response.
📍 Affects 3 files
app/services/ticket_service.py#L202-L211(this comment)app/api/public.py#L42-L61tests/api/test_public.py#L15-L29
🤖 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 `@app/services/ticket_service.py` around lines 202 - 211, Prevent private
resolved-ticket content from reaching unauthenticated public resolution: update
the resolver around generate_response in app/services/ticket_service.py (lines
202-211) to support a public-safe mode that disables private-ticket similarity
or uses a curated public corpus; invoke that mode from the public route in
app/api/public.py (lines 42-61). Add coverage in tests/api/test_public.py (lines
15-29) by creating a resolved ticket with unique private text and asserting
/resolve neither returns the text nor selects it as a similarity response.
| else: | ||
| ticket.status = TicketStatus.ESCALATED.value | ||
| ticket.response = None | ||
| logger.info( | ||
| f"Ticket {ticket.id} escalated with intent {intent} " | ||
| f"(confidence: {confidence})" | ||
| ) | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Clear response_source when escalation clears the response.
If automation reruns for a previously auto-resolved ticket, this branch leaves its old response_source persisted while response is None. Clear both fields together.
Proposed fix
else:
ticket.status = TicketStatus.ESCALATED.value
ticket.response = None
+ ticket.response_source = None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| else: | |
| ticket.status = TicketStatus.ESCALATED.value | |
| ticket.response = None | |
| logger.info( | |
| f"Ticket {ticket.id} escalated with intent {intent} " | |
| f"(confidence: {confidence})" | |
| ) | |
| else: | |
| ticket.status = TicketStatus.ESCALATED.value | |
| ticket.response = None | |
| ticket.response_source = None |
🤖 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 `@app/services/ticket_service.py` around lines 254 - 257, Update the escalation
branch in the ticket status handling to set ticket.response_source to None
alongside ticket.response when clearing an automated response, keeping both
fields synchronized.
) Final cleanup sweep after PRs #93/#94/#95. - README.md endpoint reference table still listed the pre-split paths (/tickets/{id}/assign, /tickets/{id}/close) and was missing /resolve and the accept/my-assignments rows entirely. Rebuilt the table into three sections (Public, Tickets, Agent) matching the current routes, and corrected the auth column for POST/GET /tickets — auth there has always been optional (see oauth2_scheme_optional in tickets.py), the table just never reflected that. - app/api/admin.py docstring referenced the old agent self-assign path in a comment; updated to /agent/tickets/{id}/assign. Nothing behavioral changed. Full suite: 667 passed, 0 failed.
What
Adds one free, stateless, unauthenticated endpoint:
POST /resolve. Send a message, get back intent + sentiment classification and (when confident) a ready-to-use response - no login, no API key, nothing stored.This is meant to be the thing other repos integrate against: point a client at
/resolveand you're done.How
run_ticket_automationinto a newresolve_message(message, db)function inticket_service.py. Both the existing ticket-creation flow and the new endpoint call it now - no duplicated logic, and existing ticket behavior is unchanged.app/api/public.py(router) +app/schemas/public.py(request/response models).main.py/ OpenAPI docs so it's the first thing people see in/docs.GET /landing route pointing at/docsand/resolve.tests/api/test_public.py: no-auth access, response shape, validation errors, that a bearer token is ignored, and that nothing gets persisted to the DB.Testing
Full suite: 660 passed, 0 failed (ran locally with pytest). New tests added for the endpoint itself.
Notes for reviewer
ticketstable for the similarity-search corpus - that's intentional; "stateless" here means no ticket row is created for the request, not that it ignores prior history when finding a similar past answer.Summary by CodeRabbit
New Features
POST /resolveendpoint for stateless message classification and response generation.Documentation
Bug Fixes