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