chore: add @doist/todoist-ai

dependency to package.json
اول دفعة من التطوير
This commit is contained in:
Your Name
2026-04-03 00:28:00 +02:00
parent 314f847ece
commit efb6854741
31 changed files with 2693 additions and 91 deletions

View File

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

View File

@@ -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"])

View File

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

View File

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

View File

@@ -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:

View File

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

View File

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

View File

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