Skip to content

feat: add public no-auth /resolve endpoint - #93

Merged
Yad4o merged 2 commits into
mainfrom
feat/public-resolve-endpoint
Aug 10, 2026
Merged

Yad4o merged 2 commits into
mainfrom
feat/public-resolve-endpoint

Conversation

@Yad4o

@Yad4o Yad4o commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

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 /resolve and you're done.

How

  • Extracted the core AI pipeline out of run_ticket_automation into a new resolve_message(message, db) function in ticket_service.py. Both the existing ticket-creation flow and the new endpoint call it now - no duplicated logic, and existing ticket behavior is unchanged.
  • New app/api/public.py (router) + app/schemas/public.py (request/response models).
  • Registered first in main.py / OpenAPI docs so it's the first thing people see in /docs.
  • Added a friendly GET / landing route pointing at /docs and /resolve.
  • README quickstart with curl/JS/Python snippets.
  • New 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

  • This is PR 1 of a few. Next up: separating agent-only ticket endpoints (assign/accept/close/my-assignments) into their own router away from the public one, and opening CORS globally now that auth is header-based (JWT Bearer, not cookies) so it's usable from any origin.
  • The endpoint still reads (not writes) from the tickets table 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

    • Added an unauthenticated POST /resolve endpoint for stateless message classification and response generation.
    • Responses include decision outcomes and optional intent, confidence, sentiment, and generated-response details.
    • Added a landing page with service usage and documentation links.
    • Added quickstart documentation with cURL, JavaScript, and Python examples.
  • Documentation

    • Clarified that ticket history, queues, and administrative metrics remain optional authenticated features.
  • Bug Fixes

    • Improved validation for missing or empty messages and ensured requests do not create tickets.

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.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eff09b5f-3a48-4f27-b206-e43b325359d3

📥 Commits

Reviewing files that changed from the base of the PR and between 58425ef and 62b0ec1.

📒 Files selected for processing (1)
  • app/main.py

📝 Walkthrough

Walkthrough

The PR adds an unauthenticated POST /resolve endpoint. It validates messages, applies rate limiting, runs stateless resolution, and returns decision metadata without persisting tickets. The application adds public API documentation, a landing route, and usage examples.

Changes

Public resolution API

Layer / File(s) Summary
Stateless resolution pipeline
app/schemas/public.py, app/services/ticket_service.py
Adds validated public request and response models. Extracts resolution into resolve_message and keeps ticket persistence in run_ticket_automation.
Public endpoint and application wiring
app/api/public.py, app/main.py
Adds rate-limited unauthenticated POST /resolve, registers the public router, updates API descriptions, and adds a root landing route.
Public API validation and usage
tests/api/test_public.py, README.md
Tests access, response shape, validation, bearer-token handling, and non-persistence. Documents curl, JavaScript, and Python usage.

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
Loading

Possibly related PRs

  • Yad4o/SRS#32: Modifies the ticket automation pipeline reused through resolve_message.
  • Yad4o/SRS#61: Provides the rate-limiting infrastructure used by the public endpoint.
  • Yad4o/SRS#91: Extends the shared ticket-resolution and escalation workflow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly and concisely describes the main change: adding a public unauthenticated /resolve endpoint.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/public-resolve-endpoint

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between be994fc and 58425ef.

📒 Files selected for processing (6)
  • README.md
  • app/api/public.py
  • app/main.py
  • app/schemas/public.py
  • app/services/ticket_service.py
  • tests/api/test_public.py

Comment thread app/schemas/public.py
Comment on lines +17 to +23
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?"],
)

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.

🎯 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.py

Repository: 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 tests

Repository: 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.

Comment thread app/schemas/public.py
Comment on lines +39 to +41
response_source: str | None = Field(
None, description="Where the response came from, e.g. 'template' or 'similar_ticket'."
)

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.

🎯 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.

Suggested change
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.

Comment on lines 202 to 211
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,
)

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.

🔒 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 /resolve cannot 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-L61
  • tests/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.

Comment on lines +254 to 257
else:
ticket.status = TicketStatus.ESCALATED.value
ticket.response = None
logger.info(
f"Ticket {ticket.id} escalated with intent {intent} "
f"(confidence: {confidence})"
)

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.

🗄️ 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.

Suggested change
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.

@Yad4o
Yad4o merged commit 07c4acd into main Aug 10, 2026
1 of 2 checks passed
Yad4o added a commit that referenced this pull request Aug 11, 2026
)

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.
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.

1 participant