chore: add @doist/todoist-ai
dependency to package.json اول دفعة من التطوير
This commit is contained in:
@@ -15,10 +15,17 @@ from app.services.policy_service import get_usage_summary_for_user
|
||||
from app.services.credit_config import (
|
||||
get_all_tool_costs,
|
||||
get_credits_for_plan,
|
||||
get_dynamic_tools_info,
|
||||
get_tool_credit_cost,
|
||||
CREDIT_WINDOW_DAYS,
|
||||
)
|
||||
from app.services.credit_service import deduct_credits, get_credit_summary
|
||||
from app.services.quote_service import (
|
||||
create_quote,
|
||||
estimate_quote,
|
||||
fulfill_quote,
|
||||
QuoteError,
|
||||
)
|
||||
from app.services.stripe_service import (
|
||||
is_stripe_configured,
|
||||
get_stripe_price_id,
|
||||
@@ -57,6 +64,7 @@ def get_credit_info_route():
|
||||
"pro": {"credits": get_credits_for_plan("pro"), "window_days": CREDIT_WINDOW_DAYS},
|
||||
},
|
||||
"tool_costs": get_all_tool_costs(),
|
||||
"dynamic_tools": get_dynamic_tools_info(),
|
||||
}), 200
|
||||
|
||||
|
||||
@@ -190,6 +198,7 @@ def claim_task_route():
|
||||
|
||||
Called after a guest signs up or logs in to record the previously
|
||||
processed task in their account and deduct credits.
|
||||
Uses the quote engine, so welcome bonus is applied automatically.
|
||||
"""
|
||||
user_id = get_current_user_id()
|
||||
if user_id is None:
|
||||
@@ -216,15 +225,15 @@ def claim_task_route():
|
||||
return jsonify({"error": "User not found."}), 404
|
||||
|
||||
plan = user.get("plan", "free")
|
||||
cost = get_tool_credit_cost(tool)
|
||||
|
||||
# Deduct credits
|
||||
# Use the quote engine (supports welcome bonus)
|
||||
try:
|
||||
deduct_credits(user_id, plan, tool)
|
||||
except ValueError:
|
||||
quote = create_quote(user_id, plan, tool)
|
||||
fulfill_quote(quote, user_id, plan)
|
||||
except (QuoteError, ValueError) as exc:
|
||||
return jsonify({
|
||||
"error": "Insufficient credits to claim this file.",
|
||||
"credits_required": cost,
|
||||
"error": str(exc),
|
||||
"credits_required": get_tool_credit_cost(tool),
|
||||
}), 429
|
||||
|
||||
# Record usage event so the task appears in history
|
||||
@@ -235,8 +244,38 @@ def claim_task_route():
|
||||
task_id=task_id,
|
||||
event_type="accepted",
|
||||
api_key_id=None,
|
||||
cost_points=cost,
|
||||
cost_points=quote.charged_credits,
|
||||
quoted_credits=quote.quoted_credits,
|
||||
)
|
||||
|
||||
summary = get_credit_summary(user_id, plan)
|
||||
return jsonify({"claimed": True, "credits": summary}), 200
|
||||
return jsonify({
|
||||
"claimed": True,
|
||||
"credits": summary,
|
||||
"welcome_bonus_applied": quote.welcome_bonus_applied,
|
||||
}), 200
|
||||
|
||||
|
||||
@account_bp.route("/estimate", methods=["POST"])
|
||||
@limiter.limit("120/hour")
|
||||
def estimate_cost_route():
|
||||
"""Return a non-binding credit cost estimate for a tool invocation.
|
||||
|
||||
Body: { "tool": "chat-pdf", "file_size_kb": 1024, "estimated_tokens": 5000 }
|
||||
All fields except ``tool`` are optional.
|
||||
"""
|
||||
user_id = get_current_user_id()
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
tool = str(data.get("tool", "")).strip()
|
||||
if not tool:
|
||||
return jsonify({"error": "tool is required."}), 400
|
||||
|
||||
file_size_kb = float(data.get("file_size_kb", 0))
|
||||
estimated_tokens = int(data.get("estimated_tokens", 0))
|
||||
|
||||
user = get_user_by_id(user_id) if user_id else None
|
||||
plan = user.get("plan", "free") if user else "free"
|
||||
|
||||
result = estimate_quote(user_id, plan, tool, file_size_kb, estimated_tokens)
|
||||
return jsonify(result), 200
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.services.account_service import (
|
||||
verify_and_consume_reset_token,
|
||||
update_user_password,
|
||||
)
|
||||
from app.services.credit_service import get_credit_summary
|
||||
from app.services.email_service import send_password_reset_email
|
||||
from app.utils.auth import (
|
||||
get_current_user_id,
|
||||
@@ -62,7 +63,13 @@ def register_route():
|
||||
return jsonify({"error": str(exc)}), 409
|
||||
|
||||
login_user_session(user["id"])
|
||||
return jsonify({"message": "Account created successfully.", "user": user}), 201
|
||||
credits = get_credit_summary(user["id"], user.get("plan", "free"))
|
||||
return jsonify({
|
||||
"message": "Account created successfully.",
|
||||
"user": user,
|
||||
"credits": credits,
|
||||
"is_new_account": True,
|
||||
}), 201
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["POST"])
|
||||
@@ -79,7 +86,12 @@ def login_route():
|
||||
return jsonify({"error": "Invalid email or password."}), 401
|
||||
|
||||
login_user_session(user["id"])
|
||||
return jsonify({"message": "Signed in successfully.", "user": user}), 200
|
||||
credits = get_credit_summary(user["id"], user.get("plan", "free"))
|
||||
return jsonify({
|
||||
"message": "Signed in successfully.",
|
||||
"user": user,
|
||||
"credits": credits,
|
||||
}), 200
|
||||
|
||||
|
||||
@auth_bp.route("/logout", methods=["POST"])
|
||||
@@ -103,7 +115,8 @@ def me_route():
|
||||
logout_user_session()
|
||||
return jsonify({"authenticated": False, "user": None}), 200
|
||||
|
||||
return jsonify({"authenticated": True, "user": user}), 200
|
||||
credits = get_credit_summary(user_id, user.get("plan", "free"))
|
||||
return jsonify({"authenticated": True, "user": user, "credits": credits}), 200
|
||||
|
||||
|
||||
@auth_bp.route("/csrf", methods=["GET"])
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""PDF compression routes."""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from app.extensions import limiter
|
||||
@@ -10,6 +12,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.quote_service import create_quote, QuoteError
|
||||
from app.utils.file_validator import FileValidationError
|
||||
from app.utils.sanitizer import generate_safe_path
|
||||
from app.tasks.compress_tasks import compress_pdf_task
|
||||
@@ -50,6 +53,12 @@ def compress_pdf_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "compress-pdf", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = compress_pdf_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -57,9 +66,10 @@ def compress_pdf_route():
|
||||
quality,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "compress-pdf", task.id)
|
||||
record_accepted_usage(actor, "compress-pdf", task.id, quote=quote)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task.id,
|
||||
"message": "Compression started. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}), 202
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Image compression routes."""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from app.extensions import limiter
|
||||
@@ -10,6 +12,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.quote_service import create_quote, QuoteError
|
||||
from app.utils.file_validator import FileValidationError
|
||||
from app.utils.sanitizer import generate_safe_path
|
||||
from app.tasks.compress_image_tasks import compress_image_task
|
||||
@@ -57,6 +60,12 @@ def compress_image_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "compress-image", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = compress_image_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -64,9 +73,10 @@ def compress_image_route():
|
||||
quality,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "compress-image", task.id)
|
||||
record_accepted_usage(actor, "compress-image", task.id, quote=quote)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task.id,
|
||||
"message": "Image compression started. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}), 202
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
FREE_PLAN,
|
||||
)
|
||||
from app.services.credit_config import get_dynamic_tools_info
|
||||
|
||||
config_bp = Blueprint("config", __name__)
|
||||
|
||||
@@ -24,6 +25,7 @@ def get_config():
|
||||
payload: dict = {
|
||||
"file_limits_mb": file_limits_mb,
|
||||
"max_upload_mb": max(file_limits_mb.values()),
|
||||
"dynamic_tools": get_dynamic_tools_info(),
|
||||
}
|
||||
|
||||
if actor.user_id is not None:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""OCR routes — extract text from images and PDFs."""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
|
||||
from app.extensions import limiter
|
||||
@@ -10,6 +12,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.quote_service import create_quote, QuoteError
|
||||
from app.services.ocr_service import SUPPORTED_LANGUAGES
|
||||
from app.utils.file_validator import FileValidationError
|
||||
from app.utils.sanitizer import generate_safe_path
|
||||
@@ -66,15 +69,22 @@ def ocr_image_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "ocr-image", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = ocr_image_task.delay(
|
||||
input_path, task_id, original_filename, lang,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "ocr-image", task.id)
|
||||
record_accepted_usage(actor, "ocr-image", task.id, quote=quote)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task.id,
|
||||
"message": "OCR started. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}), 202
|
||||
|
||||
|
||||
@@ -116,15 +126,22 @@ def ocr_pdf_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "ocr-pdf", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = ocr_pdf_task.delay(
|
||||
input_path, task_id, original_filename, lang,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "ocr-pdf", task.id)
|
||||
record_accepted_usage(actor, "ocr-pdf", task.id, quote=quote)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task.id,
|
||||
"message": "OCR started. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}), 202
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""PDF AI tool routes — Chat, Summarize, Translate, Table Extract."""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from app.extensions import limiter
|
||||
@@ -11,6 +13,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.quote_service import create_quote, QuoteError
|
||||
from app.services.translation_guardrails import (
|
||||
check_page_admission,
|
||||
TranslationAdmissionError,
|
||||
@@ -66,6 +69,13 @@ def chat_pdf_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "chat-pdf", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = chat_with_pdf_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -73,12 +83,13 @@ def chat_pdf_route():
|
||||
question,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "chat-pdf", task.id)
|
||||
record_accepted_usage(actor, "chat-pdf", task.id, quote=quote)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"task_id": task.id,
|
||||
"message": "Processing your question. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}
|
||||
), 202
|
||||
|
||||
@@ -122,6 +133,13 @@ def summarize_pdf_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "summarize-pdf", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = summarize_pdf_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -129,12 +147,13 @@ def summarize_pdf_route():
|
||||
length,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "summarize-pdf", task.id)
|
||||
record_accepted_usage(actor, "summarize-pdf", task.id, quote=quote)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"task_id": task.id,
|
||||
"message": "Summarizing document. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}
|
||||
), 202
|
||||
|
||||
@@ -185,6 +204,13 @@ def translate_pdf_route():
|
||||
except TranslationAdmissionError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "translate-pdf", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = translate_pdf_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -193,12 +219,13 @@ def translate_pdf_route():
|
||||
source_language,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "translate-pdf", task.id)
|
||||
record_accepted_usage(actor, "translate-pdf", task.id, quote=quote)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"task_id": task.id,
|
||||
"message": "Translating document. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}
|
||||
), 202
|
||||
|
||||
@@ -237,17 +264,25 @@ def extract_tables_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "extract-tables", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = extract_tables_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
original_filename,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "extract-tables", task.id)
|
||||
record_accepted_usage(actor, "extract-tables", task.id, quote=quote)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"task_id": task.id,
|
||||
"message": "Extracting tables. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}
|
||||
), 202
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Background removal route."""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
|
||||
from app.extensions import limiter
|
||||
@@ -10,6 +12,7 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.quote_service import create_quote, QuoteError
|
||||
from app.utils.file_validator import FileValidationError
|
||||
from app.utils.sanitizer import generate_safe_path
|
||||
from app.tasks.removebg_tasks import remove_bg_task
|
||||
@@ -52,13 +55,20 @@ def remove_bg_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
file_size_kb = os.path.getsize(input_path) / 1024
|
||||
try:
|
||||
quote = create_quote(actor.user_id, actor.plan, "remove-bg", file_size_kb=file_size_kb)
|
||||
except QuoteError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = remove_bg_task.delay(
|
||||
input_path, task_id, original_filename,
|
||||
**build_task_tracking_kwargs(actor),
|
||||
)
|
||||
record_accepted_usage(actor, "remove-bg", task.id)
|
||||
record_accepted_usage(actor, "remove-bg", task.id, quote=quote)
|
||||
|
||||
return jsonify({
|
||||
"task_id": task.id,
|
||||
"message": "Background removal started. Poll /api/tasks/{task_id}/status for progress.",
|
||||
"quote": quote.to_dict(),
|
||||
}), 202
|
||||
|
||||
Reference in New Issue
Block a user