lambda-erp 0.6.16__tar.gz → 0.6.17__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.
Files changed (84) hide show
  1. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/PKG-INFO +1 -1
  2. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/attachments.py +9 -0
  3. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/chat.py +203 -16
  4. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/pyproject.toml +1 -1
  5. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/.gitignore +0 -0
  6. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/LICENSE +0 -0
  7. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/README.md +0 -0
  8. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/__init__.py +0 -0
  9. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/auth.py +0 -0
  10. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/bootstrap.py +0 -0
  11. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/demo_limits.py +0 -0
  12. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/deps.py +0 -0
  13. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/errors.py +0 -0
  14. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/main.py +0 -0
  15. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/oauth.py +0 -0
  16. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/pdf.py +0 -0
  17. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/providers.py +0 -0
  18. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/remarks_md.py +0 -0
  19. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/__init__.py +0 -0
  20. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/accounting.py +0 -0
  21. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/admin.py +0 -0
  22. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/analytics.py +0 -0
  23. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/bank_reconciliation.py +0 -0
  24. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/chat_api.py +0 -0
  25. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/documents.py +0 -0
  26. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/masters.py +0 -0
  27. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/mcp.py +0 -0
  28. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/services.py +0 -0
  32. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/templates/document.html +0 -0
  33. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/templates/proposal.html +0 -0
  34. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/docs/agents/README.md +0 -0
  35. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/LICENSE +0 -0
  36. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/README.md +0 -0
  37. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/src/api/client.ts +0 -0
  38. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/engine.py +0 -0
  52. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/pack.py +0 -0
  53. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  54. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  55. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  56. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  57. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  58. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  59. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/profiles.py +0 -0
  60. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/spine.py +0 -0
  61. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/subscription.py +0 -0
  62. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/buying/__init__.py +0 -0
  63. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/buying/purchase_order.py +0 -0
  64. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/__init__.py +0 -0
  65. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/currency.py +0 -0
  66. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/defaults.py +0 -0
  67. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/pricing_rule.py +0 -0
  68. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  69. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/database.py +0 -0
  70. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/exceptions.py +0 -0
  71. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/hooks.py +0 -0
  72. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/model.py +0 -0
  73. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/__init__.py +0 -0
  74. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/proposal.py +0 -0
  75. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/quotation.py +0 -0
  76. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/sales_order.py +0 -0
  77. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/simulation.py +0 -0
  78. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/__init__.py +0 -0
  79. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/delivery_note.py +0 -0
  80. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/purchase_receipt.py +0 -0
  81. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/stock_entry.py +0 -0
  82. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/stock_ledger.py +0 -0
  83. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/utils.py +0 -0
  84. {lambda_erp-0.6.16 → lambda_erp-0.6.17}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.16
3
+ Version: 0.6.17
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -402,6 +402,15 @@ def build_multimodal_content(attachment: dict) -> dict:
402
402
  },
403
403
  }
404
404
  if mime in OFFICE_MIME_TYPES:
405
+ # Office bytes are read via the Files API (file_id) — but ONLY the
406
+ # Responses API accepts non-PDF file inputs. On the default Chat
407
+ # Completions backend, degrade gracefully instead of triggering a hard
408
+ # 400 (flip ERP_CHAT_API=responses to actually read Office files).
409
+ if os.environ.get("ERP_CHAT_API", "chat").strip().lower() != "responses":
410
+ return {"type": "text",
411
+ "text": (f"[Attachment “{filename}” is an Office document; the current chat "
412
+ "backend can only read PDFs and images. Convert it to PDF, or ask an "
413
+ "admin to enable the Responses backend.]")}
405
414
  file_id = ensure_openai_file(attachment)
406
415
  if file_id:
407
416
  return {"type": "file", "file": {"file_id": file_id}}
@@ -2698,6 +2698,204 @@ async def generate_title(
2698
2698
  ORCHESTRATOR_MODEL = "gpt-5.6-terra"
2699
2699
 
2700
2700
 
2701
+ # ---------------------------------------------------------------------------
2702
+ # LLM API selector: Chat Completions (default) vs Responses API
2703
+ #
2704
+ # The Responses API (/v1/responses) is what accepts non-PDF file inputs
2705
+ # (Office documents via input_file), and it does reasoning natively (no
2706
+ # reasoning_effort="none" hack). Migrating the whole loop is risky, so it's
2707
+ # behind a flag and OFF by default. To keep the blast radius tiny, ALL
2708
+ # conversation state stays Chat-Completions-shaped; we translate to/from the
2709
+ # Responses shape only at the single orchestrator call boundary
2710
+ # (_orchestrator_turn), and shim the Responses output back into the
2711
+ # Chat-shaped objects the loop and providers.py already expect.
2712
+ # ---------------------------------------------------------------------------
2713
+
2714
+ def _use_responses_api() -> bool:
2715
+ return os.environ.get("ERP_CHAT_API", "chat").strip().lower() == "responses"
2716
+
2717
+
2718
+ # Reasoning effort for the Responses path (native; the Chat path uses "none").
2719
+ _RESPONSES_REASONING_EFFORT = "low"
2720
+
2721
+
2722
+ class _ShimFn:
2723
+ def __init__(self, name, arguments):
2724
+ self.name = name
2725
+ self.arguments = arguments
2726
+
2727
+
2728
+ class _ShimToolCall:
2729
+ def __init__(self, call_id, name, arguments):
2730
+ self.id = call_id
2731
+ self.type = "function"
2732
+ self.function = _ShimFn(name, arguments)
2733
+
2734
+
2735
+ class _ShimMessage:
2736
+ """Quacks like a Chat Completions `choice.message`."""
2737
+ def __init__(self, content, tool_calls):
2738
+ self.content = content
2739
+ self.tool_calls = tool_calls or None
2740
+
2741
+
2742
+ class _ShimUsageDetails:
2743
+ def __init__(self, cached_tokens):
2744
+ self.cached_tokens = cached_tokens
2745
+
2746
+
2747
+ class _ShimUsage:
2748
+ """Quacks like a Chat Completions `response.usage` (so cost_of_openai_call
2749
+ and demo_limiter.settle read it unchanged)."""
2750
+ def __init__(self, prompt_tokens, completion_tokens, cached_tokens):
2751
+ self.prompt_tokens = prompt_tokens
2752
+ self.completion_tokens = completion_tokens
2753
+ self.prompt_tokens_details = _ShimUsageDetails(cached_tokens)
2754
+
2755
+
2756
+ def _responses_tools_from_chat(tools: list) -> list:
2757
+ """Chat tool schema {"type":"function","function":{name,...}} -> the flatter
2758
+ Responses shape {"type":"function","name",...}."""
2759
+ out = []
2760
+ for t in tools or []:
2761
+ fn = t.get("function") if isinstance(t, dict) else None
2762
+ if t.get("type") == "function" and fn:
2763
+ out.append({
2764
+ "type": "function",
2765
+ "name": fn["name"],
2766
+ "description": fn.get("description", ""),
2767
+ "parameters": fn.get("parameters") or {"type": "object", "properties": {}},
2768
+ })
2769
+ else:
2770
+ out.append(t)
2771
+ return out
2772
+
2773
+
2774
+ def _responses_msg_content(content):
2775
+ """A Chat message `content` -> Responses message content: a plain string
2776
+ passes through unchanged; a multimodal parts list becomes input_* parts
2777
+ (input_text/input_image/input_file). Only user messages carry lists."""
2778
+ if content is None:
2779
+ return ""
2780
+ if isinstance(content, str):
2781
+ return content
2782
+ parts = []
2783
+ for p in content:
2784
+ ptype = p.get("type")
2785
+ if ptype == "text":
2786
+ parts.append({"type": "input_text", "text": p.get("text", "")})
2787
+ elif ptype == "image_url":
2788
+ url = (p.get("image_url") or {}).get("url")
2789
+ parts.append({"type": "input_image", "image_url": url})
2790
+ elif ptype == "file":
2791
+ f = p.get("file") or {}
2792
+ item = {"type": "input_file"}
2793
+ if f.get("file_id"):
2794
+ item["file_id"] = f["file_id"]
2795
+ else:
2796
+ if f.get("filename"):
2797
+ item["filename"] = f["filename"]
2798
+ if f.get("file_data"):
2799
+ item["file_data"] = f["file_data"]
2800
+ parts.append(item)
2801
+ else:
2802
+ parts.append({"type": "input_text", "text": json.dumps(p, default=str)})
2803
+ return parts
2804
+
2805
+
2806
+ def _responses_request_from_chat(messages: list) -> tuple:
2807
+ """Translate Chat `messages` into (instructions, input_items) for Responses.
2808
+ The first system message becomes `instructions`; assistant tool_calls become
2809
+ `function_call` items and tool results become `function_call_output` items."""
2810
+ instructions = None
2811
+ input_items: list = []
2812
+ for m in messages:
2813
+ role = m.get("role")
2814
+ content = m.get("content")
2815
+ if role == "system":
2816
+ flat = content if isinstance(content, str) else " ".join(
2817
+ p.get("text", "") for p in (content or []) if isinstance(p, dict)
2818
+ )
2819
+ if instructions is None:
2820
+ instructions = flat
2821
+ else:
2822
+ input_items.append({"role": "developer", "content": _responses_msg_content(content)})
2823
+ elif role == "assistant":
2824
+ if content:
2825
+ input_items.append({"role": "assistant", "content": _responses_msg_content(content)})
2826
+ for tc in m.get("tool_calls") or []:
2827
+ input_items.append({
2828
+ "type": "function_call",
2829
+ "call_id": tc["id"],
2830
+ "name": tc["function"]["name"],
2831
+ "arguments": tc["function"]["arguments"],
2832
+ })
2833
+ elif role == "tool":
2834
+ input_items.append({
2835
+ "type": "function_call_output",
2836
+ "call_id": m.get("tool_call_id"),
2837
+ "output": m.get("content") or "",
2838
+ })
2839
+ else: # user
2840
+ input_items.append({"role": "user", "content": _responses_msg_content(content)})
2841
+ return instructions, input_items
2842
+
2843
+
2844
+ def _shim_message_from_responses(response) -> _ShimMessage:
2845
+ """Map a Responses result's output items into a Chat-shaped message."""
2846
+ text_parts, tool_calls = [], []
2847
+ for item in (getattr(response, "output", None) or []):
2848
+ itype = getattr(item, "type", None)
2849
+ if itype == "function_call":
2850
+ tool_calls.append(_ShimToolCall(item.call_id, item.name, item.arguments))
2851
+ elif itype == "message":
2852
+ for c in (getattr(item, "content", None) or []):
2853
+ if getattr(c, "type", None) in ("output_text", "text"):
2854
+ text_parts.append(getattr(c, "text", "") or "")
2855
+ # reasoning items are not echoed back (state is rebuilt from history each turn)
2856
+ text = "".join(text_parts) or (getattr(response, "output_text", "") or "")
2857
+ return _ShimMessage(text, tool_calls)
2858
+
2859
+
2860
+ def _shim_usage_from_responses(usage):
2861
+ if usage is None:
2862
+ return None
2863
+ inp = int(getattr(usage, "input_tokens", 0) or 0)
2864
+ out = int(getattr(usage, "output_tokens", 0) or 0)
2865
+ details = getattr(usage, "input_tokens_details", None)
2866
+ cached = int(getattr(details, "cached_tokens", 0) or 0) if details else 0
2867
+ return _ShimUsage(inp, out, cached)
2868
+
2869
+
2870
+ def _orchestrator_turn(client, messages, tools, max_tokens):
2871
+ """One orchestrator LLM turn. Returns (message, usage) shaped like Chat
2872
+ Completions, using either the Chat or Responses API per ERP_CHAT_API.
2873
+ Blocking — call via asyncio.to_thread."""
2874
+ if _use_responses_api():
2875
+ instructions, input_items = _responses_request_from_chat(messages)
2876
+ resp = client.responses.create(
2877
+ model=ORCHESTRATOR_MODEL,
2878
+ instructions=instructions,
2879
+ input=input_items,
2880
+ tools=_responses_tools_from_chat(tools),
2881
+ tool_choice="auto",
2882
+ max_output_tokens=max_tokens,
2883
+ reasoning={"effort": _RESPONSES_REASONING_EFFORT},
2884
+ )
2885
+ return _shim_message_from_responses(resp), _shim_usage_from_responses(getattr(resp, "usage", None))
2886
+ resp = client.chat.completions.create(
2887
+ model=ORCHESTRATOR_MODEL,
2888
+ messages=messages,
2889
+ tools=tools,
2890
+ tool_choice="auto",
2891
+ max_completion_tokens=max_tokens,
2892
+ # gpt-5.6-terra rejects function tools on /v1/chat/completions unless
2893
+ # reasoning_effort is "none" (400 otherwise).
2894
+ reasoning_effort="none",
2895
+ )
2896
+ return resp.choices[0].message, getattr(resp, "usage", None)
2897
+
2898
+
2701
2899
  async def run_thinking_loop(
2702
2900
  messages: list[dict],
2703
2901
  on_event,
@@ -2825,24 +3023,16 @@ async def run_thinking_loop(
2825
3023
  print(f"[chat_llm] provider=openai model={model_name} session_id={session_id or '-'} iter={iteration + 1}", flush=True)
2826
3024
  await on_event({"type": "llm_provider", "provider": "openai", "model": model_name})
2827
3025
  try:
2828
- response = await asyncio.to_thread(
2829
- openai_client.chat.completions.create,
2830
- model=model_name,
2831
- messages=messages,
2832
- tools=build_tools(),
2833
- tool_choice="auto",
2834
- max_completion_tokens=max_completion,
2835
- # gpt-5.6-terra rejects function tools on /v1/chat/completions
2836
- # unless reasoning_effort is "none" (400 otherwise). Tool
2837
- # calls run without hidden reasoning until this loop migrates
2838
- # to the Responses API.
2839
- reasoning_effort="none",
3026
+ # One orchestrator turn via Chat Completions (default) or the
3027
+ # Responses API (ERP_CHAT_API=responses). Returns Chat-shaped
3028
+ # (message, usage) either way, so everything below is unchanged.
3029
+ message, usage = await asyncio.to_thread(
3030
+ _orchestrator_turn, openai_client, messages, build_tools(), max_completion,
2840
3031
  )
2841
3032
  except Exception as e:
2842
3033
  await on_event({"type": "error", "content": f"Error calling LLM: {e}"})
2843
3034
  return
2844
3035
 
2845
- usage = getattr(response, "usage", None)
2846
3036
  demo_limiter.settle(
2847
3037
  reservation_id,
2848
3038
  actual_cost_usd=cost_of_openai_call(model_name, usage),
@@ -2859,9 +3049,6 @@ async def run_thinking_loop(
2859
3049
  if not settled:
2860
3050
  demo_limiter.release(reservation_id)
2861
3051
 
2862
- choice = response.choices[0]
2863
- message = choice.message
2864
-
2865
3052
  if not message.tool_calls:
2866
3053
  content = message.content or ""
2867
3054
  messages.append({"role": "assistant", "content": content})
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.6.16"
3
+ version = "0.6.17"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
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