lambda-erp 0.8.31__tar.gz → 0.8.33__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/PKG-INFO +1 -1
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/chat.py +202 -62
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/pyproject.toml +1 -1
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/.gitignore +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/LICENSE +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/README.md +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/attachments.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/auth.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/deps.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/errors.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/list_values.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/main.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/oauth.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/pdf.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/providers.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/actions.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/bank_statements.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/documents.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/masters.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/mcp.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/services.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/templates/document.html +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/api/tool_permissions.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/frontend/README.md +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/bank_account.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/bank_statement_import.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/camt.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/database.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.31 → lambda_erp-0.8.33}/terraform/README.md +0 -0
|
@@ -2138,7 +2138,11 @@ def _handle_create_custom_analytics_report(args, user_info: dict | None = None,
|
|
|
2138
2138
|
|
|
2139
2139
|
if not args.get("title"):
|
|
2140
2140
|
return {"error": "title is required"}
|
|
2141
|
-
|
|
2141
|
+
# ``intent`` is a tool-control field for the specialist handoff, not part
|
|
2142
|
+
# of the persisted declarative report. Keep every other key so the strict
|
|
2143
|
+
# ReportDraftPayload boundary still rejects unexpected/executable fields.
|
|
2144
|
+
payload = {key: value for key, value in args.items() if key != "intent"}
|
|
2145
|
+
return create_report_draft_record(payload, user_info, source_chat_session_id=session_id)
|
|
2142
2146
|
|
|
2143
2147
|
|
|
2144
2148
|
def _handle_get_custom_analytics_report(args, user_info: dict | None = None):
|
|
@@ -2880,6 +2884,118 @@ def _extract_json_object(text: str) -> dict:
|
|
|
2880
2884
|
return json.loads(candidate)
|
|
2881
2885
|
|
|
2882
2886
|
|
|
2887
|
+
def _report_validation_summary(exc: Exception, *, max_errors: int = 4) -> str:
|
|
2888
|
+
"""Return useful schema paths without logging report contents.
|
|
2889
|
+
|
|
2890
|
+
Pydantic's full error string embeds rejected input values. Report intents
|
|
2891
|
+
can contain customer/account labels, so provider diagnostics deliberately
|
|
2892
|
+
retain only the field path, error type, and message.
|
|
2893
|
+
"""
|
|
2894
|
+
errors = getattr(exc, "errors", None)
|
|
2895
|
+
if not callable(errors):
|
|
2896
|
+
return f"{type(exc).__name__}: {exc}"
|
|
2897
|
+
error_items = errors()
|
|
2898
|
+
parts: list[str] = []
|
|
2899
|
+
for error in error_items[:max_errors]:
|
|
2900
|
+
location = ".".join(str(part) for part in error.get("loc", ())) or "report"
|
|
2901
|
+
error_type = error.get("type") or "validation_error"
|
|
2902
|
+
message = error.get("msg") or "Invalid value"
|
|
2903
|
+
parts.append(f"{location} [{error_type}]: {message}")
|
|
2904
|
+
remaining = len(error_items) - len(parts)
|
|
2905
|
+
if remaining > 0:
|
|
2906
|
+
parts.append(f"+{remaining} more")
|
|
2907
|
+
return "; ".join(parts) or type(exc).__name__
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
def _report_validation_log_summary(exc: Exception, *, max_errors: int = 4) -> str:
|
|
2911
|
+
"""Return paths/types only, keeping model-generated labels out of logs."""
|
|
2912
|
+
errors = getattr(exc, "errors", None)
|
|
2913
|
+
if not callable(errors):
|
|
2914
|
+
return type(exc).__name__
|
|
2915
|
+
error_items = errors()
|
|
2916
|
+
parts = []
|
|
2917
|
+
for error in error_items[:max_errors]:
|
|
2918
|
+
location = ".".join(str(part) for part in error.get("loc", ())) or "report"
|
|
2919
|
+
error_type = error.get("type") or "validation_error"
|
|
2920
|
+
parts.append(f"{location}[{error_type}]")
|
|
2921
|
+
remaining = len(error_items) - len(parts)
|
|
2922
|
+
if remaining > 0:
|
|
2923
|
+
parts.append(f"+{remaining}_more")
|
|
2924
|
+
return ",".join(parts) or type(exc).__name__
|
|
2925
|
+
|
|
2926
|
+
|
|
2927
|
+
def _strip_report_model_extras(value, annotation):
|
|
2928
|
+
"""Copy a report value through its declared Pydantic model shape.
|
|
2929
|
+
|
|
2930
|
+
Dictionaries used as actual values (notably RuntimeDataRequest.filters)
|
|
2931
|
+
stay untouched. Only dictionaries representing report models are narrowed
|
|
2932
|
+
to declared fields, recursively through nested model lists/optionals. This
|
|
2933
|
+
avoids depending on the per-validation ``extra=`` override added by newer
|
|
2934
|
+
Pydantic releases; Lambda ERP still supports Pydantic 2.x generally.
|
|
2935
|
+
"""
|
|
2936
|
+
from typing import get_args, get_origin
|
|
2937
|
+
from pydantic import BaseModel
|
|
2938
|
+
|
|
2939
|
+
def is_model(candidate) -> bool:
|
|
2940
|
+
return isinstance(candidate, type) and issubclass(candidate, BaseModel)
|
|
2941
|
+
|
|
2942
|
+
if is_model(annotation):
|
|
2943
|
+
if not isinstance(value, dict):
|
|
2944
|
+
return value
|
|
2945
|
+
return {
|
|
2946
|
+
name: _strip_report_model_extras(value[name], field.annotation)
|
|
2947
|
+
for name, field in annotation.model_fields.items()
|
|
2948
|
+
if name in value
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
origin = get_origin(annotation)
|
|
2952
|
+
args = get_args(annotation)
|
|
2953
|
+
if origin is list and args and isinstance(value, list):
|
|
2954
|
+
return [_strip_report_model_extras(item, args[0]) for item in value]
|
|
2955
|
+
|
|
2956
|
+
# The report models only use unions for optional fields. Follow the model
|
|
2957
|
+
# or model-list branch when one exists; primitive unions need no traversal.
|
|
2958
|
+
for candidate in args:
|
|
2959
|
+
candidate_origin = get_origin(candidate)
|
|
2960
|
+
if is_model(candidate) or candidate_origin is list:
|
|
2961
|
+
return _strip_report_model_extras(value, candidate)
|
|
2962
|
+
return value
|
|
2963
|
+
|
|
2964
|
+
|
|
2965
|
+
def _validate_report_specialist_spec(spec: dict, *, response_id: str) -> dict:
|
|
2966
|
+
"""Normalize harmless model-only fields, then enforce the canonical model.
|
|
2967
|
+
|
|
2968
|
+
ReportDraftPayload remains strict for every stored/browser-facing report.
|
|
2969
|
+
The lenient pass exists only at this LLM boundary: it removes invented
|
|
2970
|
+
presentation hints such as ``stacked`` or ``color``. Serializing that model
|
|
2971
|
+
produces a canonical object which is immediately validated strictly again,
|
|
2972
|
+
so ignored input can never reach persistence or the browser.
|
|
2973
|
+
"""
|
|
2974
|
+
from pydantic import ValidationError
|
|
2975
|
+
from api.routers.analytics import ReportDraftPayload
|
|
2976
|
+
|
|
2977
|
+
try:
|
|
2978
|
+
return ReportDraftPayload.model_validate(spec).model_dump()
|
|
2979
|
+
except ValidationError as strict_error:
|
|
2980
|
+
extra_paths = [
|
|
2981
|
+
".".join(str(part) for part in error.get("loc", ())) or "report"
|
|
2982
|
+
for error in strict_error.errors()
|
|
2983
|
+
if error.get("type") == "extra_forbidden"
|
|
2984
|
+
]
|
|
2985
|
+
if not extra_paths:
|
|
2986
|
+
raise
|
|
2987
|
+
|
|
2988
|
+
stripped = _strip_report_model_extras(spec, ReportDraftPayload)
|
|
2989
|
+
normalized = ReportDraftPayload.model_validate(stripped).model_dump()
|
|
2990
|
+
canonical = ReportDraftPayload.model_validate(normalized).model_dump()
|
|
2991
|
+
print(
|
|
2992
|
+
"[report_specialist] "
|
|
2993
|
+
f"response_id={response_id} normalized_extra_fields={','.join(extra_paths)}",
|
|
2994
|
+
flush=True,
|
|
2995
|
+
)
|
|
2996
|
+
return canonical
|
|
2997
|
+
|
|
2998
|
+
|
|
2883
2999
|
def _generate_report_spec_via_openai(
|
|
2884
3000
|
intent: str,
|
|
2885
3001
|
existing_spec: dict | None = None,
|
|
@@ -2920,72 +3036,96 @@ def _generate_report_spec_via_openai(
|
|
|
2920
3036
|
)
|
|
2921
3037
|
user_msg = "\n\n".join(user_parts)
|
|
2922
3038
|
|
|
2923
|
-
print(
|
|
2924
|
-
f"[chat_llm] provider=openai role=report_specialist model={model}",
|
|
2925
|
-
flush=True,
|
|
2926
|
-
)
|
|
2927
3039
|
client = OpenAI(
|
|
2928
3040
|
api_key=api_key,
|
|
2929
3041
|
timeout=httpx.Timeout(120.0, connect=10.0),
|
|
2930
3042
|
)
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
try:
|
|
2947
|
-
response = client.responses.create(
|
|
2948
|
-
model=model,
|
|
2949
|
-
instructions=_REPORT_CODE_SYSTEM_PROMPT,
|
|
2950
|
-
input=user_msg,
|
|
2951
|
-
max_output_tokens=4096,
|
|
2952
|
-
reasoning={"effort": "low"},
|
|
2953
|
-
)
|
|
2954
|
-
# Log every call for the admin dashboard. Only public_manager rows
|
|
2955
|
-
# count against the demo cap — other roles are logged for
|
|
2956
|
-
# visibility but exempt from rate limiting.
|
|
2957
|
-
usage = getattr(response, "usage", None)
|
|
2958
|
-
demo_limiter.settle(
|
|
2959
|
-
reservation_id,
|
|
2960
|
-
actual_cost_usd=cost_of_openai_call(model, usage),
|
|
2961
|
-
ip=client_ip or "unknown",
|
|
2962
|
-
role=user_role,
|
|
2963
|
-
provider="openai",
|
|
2964
|
-
model=model,
|
|
2965
|
-
prompt_tokens=int(getattr(usage, "input_tokens", 0) or 0) if usage else 0,
|
|
2966
|
-
completion_tokens=int(getattr(usage, "output_tokens", 0) or 0) if usage else 0,
|
|
2967
|
-
session_id=session_id,
|
|
2968
|
-
)
|
|
2969
|
-
settled = True
|
|
2970
|
-
finally:
|
|
2971
|
-
if not settled:
|
|
2972
|
-
demo_limiter.release(reservation_id)
|
|
2973
|
-
|
|
2974
|
-
text = getattr(response, "output_text", "") or ""
|
|
2975
|
-
if not text:
|
|
2976
|
-
text = "".join(
|
|
2977
|
-
getattr(block, "text", "") or ""
|
|
2978
|
-
for item in (getattr(response, "output", None) or [])
|
|
2979
|
-
for block in (getattr(item, "content", None) or [])
|
|
2980
|
-
if getattr(block, "type", "") in ("output_text", "text")
|
|
3043
|
+
validation_feedback: str | None = None
|
|
3044
|
+
max_attempts = 2
|
|
3045
|
+
|
|
3046
|
+
for attempt in range(1, max_attempts + 1):
|
|
3047
|
+
attempt_input = user_msg
|
|
3048
|
+
if validation_feedback:
|
|
3049
|
+
attempt_input += (
|
|
3050
|
+
"\n\n## Validation errors from the previous attempt\n"
|
|
3051
|
+
+ validation_feedback
|
|
3052
|
+
+ "\nRegenerate the complete report spec and correct these errors."
|
|
3053
|
+
)
|
|
3054
|
+
|
|
3055
|
+
print(
|
|
3056
|
+
f"[chat_llm] provider=openai role=report_specialist model={model} attempt={attempt}",
|
|
3057
|
+
flush=True,
|
|
2981
3058
|
)
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3059
|
+
reservation_id = None
|
|
3060
|
+
if is_demo_role(user_role):
|
|
3061
|
+
_blocked, reservation_id = demo_limiter.reserve(
|
|
3062
|
+
client_ip or "unknown",
|
|
3063
|
+
estimated_usd=demo_call_reserve_usd(),
|
|
3064
|
+
role=user_role,
|
|
3065
|
+
)
|
|
3066
|
+
if _blocked:
|
|
3067
|
+
raise RuntimeError(_blocked)
|
|
3068
|
+
|
|
3069
|
+
# try/finally + `settled` flag guarantees reservation release even if
|
|
3070
|
+
# a CancelledError slips between the SDK call returning and settle()
|
|
3071
|
+
# completing. Without this, a cancelled coroutine could leak the
|
|
3072
|
+
# reservation for the process lifetime (or until TTL sweep).
|
|
3073
|
+
settled = False
|
|
3074
|
+
try:
|
|
3075
|
+
response = client.responses.create(
|
|
3076
|
+
model=model,
|
|
3077
|
+
instructions=_REPORT_CODE_SYSTEM_PROMPT,
|
|
3078
|
+
input=attempt_input,
|
|
3079
|
+
max_output_tokens=4096,
|
|
3080
|
+
reasoning={"effort": "low"},
|
|
3081
|
+
)
|
|
3082
|
+
# Log every call for the admin dashboard. Only public_manager rows
|
|
3083
|
+
# count against the demo cap — other roles are logged for
|
|
3084
|
+
# visibility but exempt from rate limiting.
|
|
3085
|
+
usage = getattr(response, "usage", None)
|
|
3086
|
+
demo_limiter.settle(
|
|
3087
|
+
reservation_id,
|
|
3088
|
+
actual_cost_usd=cost_of_openai_call(model, usage),
|
|
3089
|
+
ip=client_ip or "unknown",
|
|
3090
|
+
role=user_role,
|
|
3091
|
+
provider="openai",
|
|
3092
|
+
model=model,
|
|
3093
|
+
prompt_tokens=int(getattr(usage, "input_tokens", 0) or 0) if usage else 0,
|
|
3094
|
+
completion_tokens=int(getattr(usage, "output_tokens", 0) or 0) if usage else 0,
|
|
3095
|
+
session_id=session_id,
|
|
3096
|
+
)
|
|
3097
|
+
settled = True
|
|
3098
|
+
finally:
|
|
3099
|
+
if not settled:
|
|
3100
|
+
demo_limiter.release(reservation_id)
|
|
3101
|
+
|
|
3102
|
+
response_id = str(getattr(response, "id", None) or "unknown")
|
|
3103
|
+
try:
|
|
3104
|
+
text = getattr(response, "output_text", "") or ""
|
|
3105
|
+
if not text:
|
|
3106
|
+
text = "".join(
|
|
3107
|
+
getattr(block, "text", "") or ""
|
|
3108
|
+
for item in (getattr(response, "output", None) or [])
|
|
3109
|
+
for block in (getattr(item, "content", None) or [])
|
|
3110
|
+
if getattr(block, "type", "") in ("output_text", "text")
|
|
3111
|
+
)
|
|
3112
|
+
spec = _extract_json_object(text)
|
|
3113
|
+
return _validate_report_specialist_spec(spec, response_id=response_id)
|
|
3114
|
+
except Exception as exc:
|
|
3115
|
+
validation_feedback = _report_validation_summary(exc)
|
|
3116
|
+
print(
|
|
3117
|
+
"[report_specialist] "
|
|
3118
|
+
f"response_id={response_id} attempt={attempt} "
|
|
3119
|
+
f"validation_error={_report_validation_log_summary(exc)}",
|
|
3120
|
+
flush=True,
|
|
3121
|
+
)
|
|
3122
|
+
if attempt == max_attempts:
|
|
3123
|
+
raise RuntimeError(
|
|
3124
|
+
"Report specialist returned an invalid spec after "
|
|
3125
|
+
f"{max_attempts} attempts: {validation_feedback}"
|
|
3126
|
+
) from exc
|
|
3127
|
+
|
|
3128
|
+
raise RuntimeError("Report specialist did not return a report spec.")
|
|
2989
3129
|
|
|
2990
3130
|
|
|
2991
3131
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|