lambda-erp 0.1.37__tar.gz → 0.1.39__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.1.37 → lambda_erp-0.1.39}/PKG-INFO +5 -5
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/README.md +4 -4
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/chat.py +49 -24
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/providers.py +2 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/chat_api.py +29 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/pyproject.toml +1 -1
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/.gitignore +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/LICENSE +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/attachments.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/auth.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/deps.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/errors.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/main.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/oauth.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/pdf.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/remarks_md.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/services.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/templates/document.html +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/frontend/README.md +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.37 → lambda_erp-0.1.39}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.39
|
|
4
4
|
Summary: Core ERP logic - accounting, sales, purchasing, inventory
|
|
5
5
|
Author: TORUS INVESTMENTS AG
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
**Open-source ERP you can run through chat — configurable in plain language**
|
|
31
31
|
|
|
32
|
-
Lambda ERP is a
|
|
32
|
+
Lambda ERP is a simpler ERP: create invoices, check inventory, answer accounting questions, and change reports by asking for what you need in plain language.
|
|
33
33
|
|
|
34
34
|
https://github.com/user-attachments/assets/1b2749ef-10e7-42f5-9cce-df5628292667
|
|
35
35
|
|
|
@@ -54,7 +54,7 @@ The two packages publish in lockstep on every release:
|
|
|
54
54
|
<a href="https://www.npmjs.com/package/@lambda-development/erp-core"><img alt="@lambda-development/erp-core on npm" src="https://img.shields.io/npm/v/@lambda-development/erp-core?label=npm%20%7C%20%40lambda-development%2Ferp-core&logo=npm"></a>
|
|
55
55
|
</p>
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Want to see them wired together? **[lambda-erp-example](https://github.com/lambdadevelopment/lambda-erp-example)** is a minimal reference deployment that consumes both packages via version pins (no fork, no vendoring) — a ready-made template for your own deployment.
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -126,7 +126,7 @@ Each of those is hours of skilled work today. With a capable LLM in the loop, th
|
|
|
126
126
|
|---|---|
|
|
127
127
|
| Backend business logic | Pure Python, no framework (`lambda_erp/`) |
|
|
128
128
|
| Web API | FastAPI + Pydantic |
|
|
129
|
-
| Storage | SQLite
|
|
129
|
+
| Storage | SQLite by default; Postgres for production |
|
|
130
130
|
| Frontend | React + Vite + TypeScript + Tailwind + Recharts |
|
|
131
131
|
| Chat transport | WebSocket |
|
|
132
132
|
| LLM orchestrator | OpenAI (configurable) |
|
|
@@ -453,7 +453,7 @@ Release notes live in [CHANGELOG.md](./CHANGELOG.md). Releases are tagged
|
|
|
453
453
|
|
|
454
454
|
## Status
|
|
455
455
|
|
|
456
|
-
Version 0
|
|
456
|
+
Version 0, and it already implements the vision: invoicing, inventory, accounting, and reporting — all driven by chat. Good for demos, internal tools, and real day-to-day work. It's under active development and improving fast.
|
|
457
457
|
|
|
458
458
|
If you try it, we'd love to know what broke.
|
|
459
459
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Open-source ERP you can run through chat — configurable in plain language**
|
|
4
4
|
|
|
5
|
-
Lambda ERP is a
|
|
5
|
+
Lambda ERP is a simpler ERP: create invoices, check inventory, answer accounting questions, and change reports by asking for what you need in plain language.
|
|
6
6
|
|
|
7
7
|
https://github.com/user-attachments/assets/1b2749ef-10e7-42f5-9cce-df5628292667
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ The two packages publish in lockstep on every release:
|
|
|
27
27
|
<a href="https://www.npmjs.com/package/@lambda-development/erp-core"><img alt="@lambda-development/erp-core on npm" src="https://img.shields.io/npm/v/@lambda-development/erp-core?label=npm%20%7C%20%40lambda-development%2Ferp-core&logo=npm"></a>
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Want to see them wired together? **[lambda-erp-example](https://github.com/lambdadevelopment/lambda-erp-example)** is a minimal reference deployment that consumes both packages via version pins (no fork, no vendoring) — a ready-made template for your own deployment.
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
@@ -99,7 +99,7 @@ Each of those is hours of skilled work today. With a capable LLM in the loop, th
|
|
|
99
99
|
|---|---|
|
|
100
100
|
| Backend business logic | Pure Python, no framework (`lambda_erp/`) |
|
|
101
101
|
| Web API | FastAPI + Pydantic |
|
|
102
|
-
| Storage | SQLite
|
|
102
|
+
| Storage | SQLite by default; Postgres for production |
|
|
103
103
|
| Frontend | React + Vite + TypeScript + Tailwind + Recharts |
|
|
104
104
|
| Chat transport | WebSocket |
|
|
105
105
|
| LLM orchestrator | OpenAI (configurable) |
|
|
@@ -426,7 +426,7 @@ Release notes live in [CHANGELOG.md](./CHANGELOG.md). Releases are tagged
|
|
|
426
426
|
|
|
427
427
|
## Status
|
|
428
428
|
|
|
429
|
-
Version 0
|
|
429
|
+
Version 0, and it already implements the vision: invoicing, inventory, accounting, and reporting — all driven by chat. Good for demos, internal tools, and real day-to-day work. It's under active development and improving fast.
|
|
430
430
|
|
|
431
431
|
If you try it, we'd love to know what broke.
|
|
432
432
|
|
|
@@ -1623,12 +1623,47 @@ def _prompt_analytics_context() -> str:
|
|
|
1623
1623
|
)
|
|
1624
1624
|
|
|
1625
1625
|
|
|
1626
|
-
def build_system_prompt(user_info: dict | None = None):
|
|
1626
|
+
def build_system_prompt(user_info: dict | None = None, channel: str = "web"):
|
|
1627
1627
|
user_name = user_info.get("full_name", "User") if user_info else "User"
|
|
1628
1628
|
user_role = user_info.get("role", "viewer") if user_info else "viewer"
|
|
1629
1629
|
company_context = _prompt_company_context()
|
|
1630
1630
|
analytics_context = _prompt_analytics_context()
|
|
1631
1631
|
|
|
1632
|
+
# Channel-aware link guidance. On the "web" channel the reader is a browser
|
|
1633
|
+
# inside the ERP, so web-relative links are clickable. On the "api" channel the
|
|
1634
|
+
# reply is relayed to an EXTERNAL app (lambda-web / the Lambda app) that is not
|
|
1635
|
+
# logged into the ERP web UI — `/app` and `/masters` links are dead there, so we
|
|
1636
|
+
# tell the agent to name records by their identifier instead. The one link we
|
|
1637
|
+
# DO keep is the canonical `/api/documents/{slug}/{name}/pdf` PDF link: the
|
|
1638
|
+
# calling platform detects it, fetches the bytes over the Bearer document API,
|
|
1639
|
+
# and re-delivers the PDF as a native attachment. See docs/chat-api.md.
|
|
1640
|
+
if channel == "api":
|
|
1641
|
+
markdown_links_section = """## Referring to records
|
|
1642
|
+
Your reply is relayed to an external application (the user's Lambda assistant / app), NOT the ERP web interface. Links into the ERP web UI are NOT clickable there, so refer to every record by its human identifier in plain text — e.g. "Quotation **QTN-2298**", "Sales Invoice **SINV-0012**". Do not paste `/app/...`, `/masters/...`, or `/reports/...` URLs; they do not work outside the ERP."""
|
|
1643
|
+
document_links_section = """## Delivering document PDFs
|
|
1644
|
+
When the user wants a document as a file (an invoice, quotation, delivery note, etc.), include exactly one canonical PDF reference in the form `/api/documents/{doctype-slug}/{name}/pdf` — e.g. `/api/documents/sales-invoice/SINV-0001/pdf`. The platform detects this reference, fetches the PDF, and delivers it to the user as a downloadable attachment. So phrase it as the file being provided ("I've attached the PDF of QTN-2298"), NOT as a link to click. Do not include `/app/...` view links or `/masters/...` links — name the record instead."""
|
|
1645
|
+
else:
|
|
1646
|
+
markdown_links_section = """## Always use markdown links
|
|
1647
|
+
Every URL you mention in chat MUST be written as a markdown link `[label](url)` — never a bare URL on its own. The chat UI only turns `[label](url)` into a proper clickable link. A bare `/reports/analytics?report_id=...` still works (a fallback linkifier catches it), but markdown form is the expected shape.
|
|
1648
|
+
|
|
1649
|
+
Examples:
|
|
1650
|
+
- Correct: `[Open report](/reports/analytics?report_id=RPT-AB12CD34)`
|
|
1651
|
+
- Correct: `[SINV-0012](/app/sales-invoice/SINV-0012)`
|
|
1652
|
+
- Avoid: `/reports/analytics?report_id=RPT-AB12CD34` (bare URL)"""
|
|
1653
|
+
document_links_section = """## Document & Master Links
|
|
1654
|
+
When referencing records, always use clickable markdown links so the user can open them directly.
|
|
1655
|
+
|
|
1656
|
+
**Documents** (quotations, invoices, orders, deliveries, receipts, payments, journal entries, stock entries):
|
|
1657
|
+
- **View/edit link:** `/app/{doctype-slug}/{name}` — e.g. [SINV-0001](/app/sales-invoice/SINV-0001)
|
|
1658
|
+
- **PDF link:** `/api/documents/{doctype-slug}/{name}/pdf` — e.g. [Download PDF](/api/documents/sales-invoice/SINV-0001/pdf)
|
|
1659
|
+
The doctype slug is the lowercase, hyphenated form: sales-invoice, purchase-order, delivery-note, etc.
|
|
1660
|
+
|
|
1661
|
+
**Master records** (customer, supplier, item, warehouse, company):
|
|
1662
|
+
- **View/edit link:** `/masters/{master-type}/{name}` — e.g. [SUPP-001](/masters/supplier/SUPP-001), [CUST-003](/masters/customer/CUST-003), [ITEM-001](/masters/item/ITEM-001)
|
|
1663
|
+
- NEVER use `/app/...` for masters — that path is only for transactional documents.
|
|
1664
|
+
|
|
1665
|
+
Always include the view link after creating, submitting, converting, or updating a record. Include the PDF link when the user asks for a printable version or when sharing an invoice/quotation."""
|
|
1666
|
+
|
|
1632
1667
|
if user_role == "admin":
|
|
1633
1668
|
role_desc = "You have **admin** access — full permissions to create, edit, submit, cancel documents, manage master data, run reports, and manage users."
|
|
1634
1669
|
elif user_role == "manager":
|
|
@@ -1654,13 +1689,7 @@ Important constraint on the analytics report tool: the JS transform runs **clien
|
|
|
1654
1689
|
|
|
1655
1690
|
When you do build a custom report, pass a plain-language `intent` to `create_custom_analytics_report` (a specialist model writes the code for you) and reply with the returned `/reports/analytics?report_id=…` link as a markdown link. The draft appears under **Custom Analytics** in the sidebar so the user can reopen or share it.
|
|
1656
1691
|
|
|
1657
|
-
|
|
1658
|
-
Every URL you mention in chat MUST be written as a markdown link `[label](url)` — never a bare URL on its own. The chat UI only turns `[label](url)` into a proper clickable link. A bare `/reports/analytics?report_id=...` still works (a fallback linkifier catches it), but markdown form is the expected shape.
|
|
1659
|
-
|
|
1660
|
-
Examples:
|
|
1661
|
-
- Correct: `[Open report](/reports/analytics?report_id=RPT-AB12CD34)`
|
|
1662
|
-
- Correct: `[SINV-0012](/app/sales-invoice/SINV-0012)`
|
|
1663
|
-
- Avoid: `/reports/analytics?report_id=RPT-AB12CD34` (bare URL)
|
|
1692
|
+
{markdown_links_section}
|
|
1664
1693
|
|
|
1665
1694
|
## Current User
|
|
1666
1695
|
You are speaking with **{user_name}** (role: **{user_role}**).
|
|
@@ -1903,19 +1932,7 @@ If a `create_document` / `update_document` call fails with an "item not found" /
|
|
|
1903
1932
|
- Be concise but helpful
|
|
1904
1933
|
- If a tool call fails, explain the error clearly and suggest how to fix it
|
|
1905
1934
|
|
|
1906
|
-
|
|
1907
|
-
When referencing records, always use clickable markdown links so the user can open them directly.
|
|
1908
|
-
|
|
1909
|
-
**Documents** (quotations, invoices, orders, deliveries, receipts, payments, journal entries, stock entries):
|
|
1910
|
-
- **View/edit link:** `/app/{{doctype-slug}}/{{name}}` — e.g. [SINV-0001](/app/sales-invoice/SINV-0001)
|
|
1911
|
-
- **PDF link:** `/api/documents/{{doctype-slug}}/{{name}}/pdf` — e.g. [Download PDF](/api/documents/sales-invoice/SINV-0001/pdf)
|
|
1912
|
-
The doctype slug is the lowercase, hyphenated form: sales-invoice, purchase-order, delivery-note, etc.
|
|
1913
|
-
|
|
1914
|
-
**Master records** (customer, supplier, item, warehouse, company):
|
|
1915
|
-
- **View/edit link:** `/masters/{{master-type}}/{{name}}` — e.g. [SUPP-001](/masters/supplier/SUPP-001), [CUST-003](/masters/customer/CUST-003), [ITEM-001](/masters/item/ITEM-001)
|
|
1916
|
-
- NEVER use `/app/...` for masters — that path is only for transactional documents.
|
|
1917
|
-
|
|
1918
|
-
Always include the view link after creating, submitting, converting, or updating a record. Include the PDF link when the user asks for a printable version or when sharing an invoice/quotation.
|
|
1935
|
+
{document_links_section}
|
|
1919
1936
|
|
|
1920
1937
|
## File Attachments
|
|
1921
1938
|
Users can attach PDFs and images (receipts, bills, contracts, screenshots) to their messages using the paperclip button in the chat input. When attachments are sent, they appear as multimodal content in the current message and you can directly see their contents.
|
|
@@ -2254,7 +2271,7 @@ async def run_thinking_loop(
|
|
|
2254
2271
|
):
|
|
2255
2272
|
"""Run the agentic reasoning loop.
|
|
2256
2273
|
|
|
2257
|
-
The orchestrator is always OpenAI (gpt-5.
|
|
2274
|
+
The orchestrator is always OpenAI (gpt-5.6-terra). When GPT decides to call
|
|
2258
2275
|
`create_custom_analytics_report` or `update_custom_analytics_report`
|
|
2259
2276
|
with an intent/feedback hint, the tool handler itself delegates the
|
|
2260
2277
|
code-generation step to Anthropic (ANTHROPIC_CODE_MODEL). We emit an
|
|
@@ -2351,7 +2368,7 @@ async def run_thinking_loop(
|
|
|
2351
2368
|
# on ANY exit path — including asyncio.CancelledError raised after
|
|
2352
2369
|
# the SDK call has already returned but before settle() ran.
|
|
2353
2370
|
settled = False
|
|
2354
|
-
model_name = "gpt-5.
|
|
2371
|
+
model_name = "gpt-5.6-terra"
|
|
2355
2372
|
try:
|
|
2356
2373
|
print(f"[chat_llm] provider=openai model={model_name} session_id={session_id or '-'} iter={iteration + 1}", flush=True)
|
|
2357
2374
|
await on_event({"type": "llm_provider", "provider": "openai", "model": model_name})
|
|
@@ -2571,6 +2588,7 @@ async def run_session_turn(
|
|
|
2571
2588
|
attachments_user_id: str | None = None,
|
|
2572
2589
|
client_ip: str | None = None,
|
|
2573
2590
|
replay_history: bool = True,
|
|
2591
|
+
channel: str = "web",
|
|
2574
2592
|
on_assistant_message=None,
|
|
2575
2593
|
on_title=None,
|
|
2576
2594
|
) -> str | None:
|
|
@@ -2586,6 +2604,13 @@ async def run_session_turn(
|
|
|
2586
2604
|
replayed (stateless REST default). The turn is still persisted by the
|
|
2587
2605
|
caller for audit — it just isn't fed back to the model.
|
|
2588
2606
|
|
|
2607
|
+
``channel`` selects the link/formatting guidance in the system prompt:
|
|
2608
|
+
- "web" (default) -> the reader is a browser inside the ERP; clickable
|
|
2609
|
+
web-relative links (`/app/…`, `/masters/…`) are used.
|
|
2610
|
+
- "api" -> the reply is relayed to an external app (the chat API);
|
|
2611
|
+
the agent names records in plain text and only emits the canonical PDF
|
|
2612
|
+
reference the caller re-delivers as an attachment.
|
|
2613
|
+
|
|
2589
2614
|
Hooks (all optional, async):
|
|
2590
2615
|
- ``on_event(event)`` streamed loop events (WS -> socket; REST -> no-op)
|
|
2591
2616
|
- ``on_assistant_message(sid, content)`` fired after the reply is persisted
|
|
@@ -2593,7 +2618,7 @@ async def run_session_turn(
|
|
|
2593
2618
|
"""
|
|
2594
2619
|
is_first_reply = count_assistant_messages(session_id) == 0
|
|
2595
2620
|
|
|
2596
|
-
messages = [{"role": "system", "content": build_system_prompt(user_info)}]
|
|
2621
|
+
messages = [{"role": "system", "content": build_system_prompt(user_info, channel=channel)}]
|
|
2597
2622
|
|
|
2598
2623
|
if replay_history:
|
|
2599
2624
|
conversation = build_conversation(session_id, limit=20)
|
|
@@ -25,6 +25,8 @@ OPENAI_PRICING: dict[str, dict[str, Any]] = {
|
|
|
25
25
|
{"max_input_tokens": None, "input": 5.00, "cached_input": 0.50, "output": 22.50},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
28
|
+
# GPT-5.6 Terra — flat pricing (no 272K tier step-up, unlike gpt-5.4).
|
|
29
|
+
"gpt-5.6-terra": {"input": 2.50, "cached_input": 0.25, "output": 15.00},
|
|
28
30
|
"gpt-4.1-nano": {"input": 0.10, "cached_input": 0.025, "output": 0.40},
|
|
29
31
|
"gpt-4.1-mini": {"input": 0.40, "cached_input": 0.10, "output": 1.60},
|
|
30
32
|
"gpt-4.1": {"input": 2.00, "cached_input": 0.50, "output": 8.00},
|
|
@@ -15,6 +15,8 @@ Statefulness (see docs/chat-api-plan.md):
|
|
|
15
15
|
- with `session_id` -> stateful: that session's history is replayed —
|
|
16
16
|
caller-owned, ephemeral working memory for a single multi-step ERP task.
|
|
17
17
|
"""
|
|
18
|
+
import re
|
|
19
|
+
|
|
18
20
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
19
21
|
from fastapi.responses import Response
|
|
20
22
|
from pydantic import BaseModel
|
|
@@ -39,6 +41,31 @@ class ChatApiRequest(BaseModel):
|
|
|
39
41
|
session_id: str | None = None
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
# The agent references document PDFs as `/api/documents/{slug}/{name}/pdf` (a
|
|
45
|
+
# cookie-gated web path). An API caller can't open that, so we surface each one as
|
|
46
|
+
# a structured `documents` entry whose `pdf_url` points at the Bearer-gated v1
|
|
47
|
+
# document endpoint the caller CAN fetch. This is the machine-readable contract the
|
|
48
|
+
# orchestrator uses to attach PDFs, instead of re-parsing the prose itself.
|
|
49
|
+
_PDF_LINK_RE = re.compile(r"/api/documents/([^/()\s]+)/([^/()\s]+)/pdf")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _extract_documents(reply: str, request: Request) -> list[dict]:
|
|
53
|
+
"""Pull referenced document PDFs out of a reply as absolute, fetchable refs."""
|
|
54
|
+
base = str(request.base_url).rstrip("/")
|
|
55
|
+
seen: set[tuple[str, str]] = set()
|
|
56
|
+
documents: list[dict] = []
|
|
57
|
+
for doctype_slug, name in _PDF_LINK_RE.findall(reply or ""):
|
|
58
|
+
if (doctype_slug, name) in seen:
|
|
59
|
+
continue
|
|
60
|
+
seen.add((doctype_slug, name))
|
|
61
|
+
documents.append({
|
|
62
|
+
"doctype": doctype_slug,
|
|
63
|
+
"name": name,
|
|
64
|
+
"pdf_url": f"{base}/api/v1/documents/{doctype_slug}/{name}/pdf",
|
|
65
|
+
})
|
|
66
|
+
return documents
|
|
67
|
+
|
|
68
|
+
|
|
42
69
|
async def _noop_event(event: dict) -> None:
|
|
43
70
|
"""Discard streamed loop events — the REST response is the final reply only."""
|
|
44
71
|
return None
|
|
@@ -85,6 +112,7 @@ async def chat(payload: ChatApiRequest, request: Request, caller: dict = Depends
|
|
|
85
112
|
_noop_event,
|
|
86
113
|
client_ip=_client_ip(request),
|
|
87
114
|
replay_history=replay_history,
|
|
115
|
+
channel="api",
|
|
88
116
|
)
|
|
89
117
|
|
|
90
118
|
session = get_session(target_session_id)
|
|
@@ -92,6 +120,7 @@ async def chat(payload: ChatApiRequest, request: Request, caller: dict = Depends
|
|
|
92
120
|
"reply": reply or "",
|
|
93
121
|
"session_id": target_session_id,
|
|
94
122
|
"title": session["title"] if session else None,
|
|
123
|
+
"documents": _extract_documents(reply or "", request),
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
|
|
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
|