feat(agent): integrate procurement AI agent with Bale bot, real-time draft sync, and git history bundle
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"intent": "register_quote",
|
||||
"patch": {
|
||||
"supplier_name": "شرکت فولاد مبارکه",
|
||||
"project_name": "برج آسمان",
|
||||
"project_names": ["برج آسمان"],
|
||||
"is_official": true,
|
||||
"notes": "تحویل پای کارگاه",
|
||||
"expiry_date": null,
|
||||
"items": [
|
||||
{
|
||||
"material_name": "میلگرد",
|
||||
"brand_name": null,
|
||||
"specs": ["A3", "سایز ۱۴"],
|
||||
"quantity": 5000,
|
||||
"unit_name": "کیلوگرم",
|
||||
"unit_price": 28500,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"payment_terms": [
|
||||
{
|
||||
"label": "شرایط پرداخت",
|
||||
"milestones": [
|
||||
{"due_date": null, "amount": 50000000},
|
||||
{"due_date": null, "amount": null}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"confidence": 0.95
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
api_key = "sk-a671eab15be846d494bf0839070a3b32"
|
||||
url = "https://api.deepseek.com/chat/completions"
|
||||
|
||||
prompt = """تو دستیار تدارکات یک ERP فارسی هستی. ورودی تو پیام صوتی/متنی کاربر است.
|
||||
یک JSON و فقط JSON برگردان (بدون توضیح اضافه) با این شکل:
|
||||
{
|
||||
"intent": "register_quote",
|
||||
"patch": {
|
||||
"supplier_name": "نام تأمینکننده",
|
||||
"project_name": "نام پروژه",
|
||||
"project_names": ["نام پروژه"],
|
||||
"is_official": true,
|
||||
"notes": "یادداشتها",
|
||||
"expiry_date": "YYYY-MM-DD",
|
||||
"items": [
|
||||
{
|
||||
"material_name": "فقط نام کلی کالا",
|
||||
"brand_name": "نام کارخانه یا برند",
|
||||
"specs": ["مشخصه ۱", "مشخصه ۲"],
|
||||
"quantity": 5000,
|
||||
"unit_name": "کیلوگرم",
|
||||
"unit_price": 28500,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"payment_terms": [
|
||||
{
|
||||
"label": "شرایط پرداخت",
|
||||
"milestones": [
|
||||
{"due_date": "YYYY-MM-DD", "amount": 50000000}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"confidence": 0.95
|
||||
}
|
||||
قوانین:
|
||||
- برای کالاها: material_name فقط نام کلی کالا باشد (مانند «میلگرد» یا «لوله»)؛ کارخانه یا برند در brand_name؛ و مشخصات فنی (مثل سایز یا گرید) در آرایه specs قرار گیرد.
|
||||
- برای شرایط پرداخت: در صورت ذکر اقساط/موعد و مبالغ، در milestones: [{"due_date": "YYYY-MM-DD", "amount": number}] قرار بده.
|
||||
"""
|
||||
|
||||
user_msg = "استعلام قیمت از شرکت فولاد مبارکه برای ۵ تن میلگرد A3 سایز ۱۴ به قیمت هر کیلو ۲۸۵۰۰ تومن، تحویل پای کارگاه برج آسمان، پرداخت در دو مرحله ۵۰ میلیون نقد و مابقی تا آخر ماه"
|
||||
|
||||
payload = {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": user_msg}
|
||||
],
|
||||
"response_format": {"type": "json_object"}
|
||||
}
|
||||
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
})
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req)
|
||||
res_data = json.loads(resp.read().decode("utf-8"))
|
||||
content = res_data["choices"][0]["message"]["content"]
|
||||
with open("tests/deepseek_output.json", "w", encoding="utf-8") as out_f:
|
||||
out_f.write(content)
|
||||
print("DEEPSEEK SUCCESS! Saved to tests/deepseek_output.json")
|
||||
print("KEYS IN EXTRACTED JSON:", list(json.loads(content).get("patch", {}).keys()))
|
||||
except Exception as e:
|
||||
print("DEEPSEEK ERROR:", e)
|
||||
@@ -0,0 +1,123 @@
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
# Stub external dependencies if not present in host python
|
||||
for mod in ["sqlalchemy", "sqlalchemy.ext", "sqlalchemy.ext.asyncio", "sqlalchemy.orm", "asyncpg", "pydantic", "pydantic_settings", "httpx"]:
|
||||
if mod not in sys.modules:
|
||||
m = types.ModuleType(mod)
|
||||
sys.modules[mod] = m
|
||||
|
||||
sys.modules["pydantic_settings"].BaseSettings = type("BaseSettings", (), {})
|
||||
sys.modules["pydantic_settings"].SettingsConfigDict = lambda **k: None
|
||||
sys.modules["pydantic"].BaseModel = type("BaseModel", (), {})
|
||||
|
||||
# Stub specific symbols needed by app.db and app.models
|
||||
sys.modules["sqlalchemy"].select = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy"].update = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy"].func = types.SimpleNamespace(gen_random_uuid=lambda: None, now=lambda: None)
|
||||
sys.modules["sqlalchemy"].BigInteger = type("BigInteger", (), {})
|
||||
sys.modules["sqlalchemy"].DateTime = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy"].Index = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy"].Integer = type("Integer", (), {})
|
||||
sys.modules["sqlalchemy"].String = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy"].Text = type("Text", (), {})
|
||||
sys.modules["sqlalchemy.dialects.postgresql"] = types.ModuleType("sqlalchemy.dialects.postgresql")
|
||||
sys.modules["sqlalchemy.dialects.postgresql"].JSONB = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy.dialects.postgresql"].UUID = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy.orm"].DeclarativeBase = type("DeclarativeBase", (), {})
|
||||
sys.modules["sqlalchemy.orm"].Mapped = type("Mapped", (), {})
|
||||
sys.modules["sqlalchemy.orm"].mapped_column = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy.ext.asyncio"].create_async_engine = lambda *a, **k: None
|
||||
sys.modules["sqlalchemy.ext.asyncio"].async_sessionmaker = lambda *a, **k: None
|
||||
|
||||
# Add 'ai procurement' to sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "ai procurement"))
|
||||
|
||||
from app.drafts import (
|
||||
new_quote_payload,
|
||||
merge_quote_patch,
|
||||
build_quote_request,
|
||||
compute_quote_missing,
|
||||
)
|
||||
|
||||
def test_wizard_conversion():
|
||||
mock_path = Path(__file__).resolve().parents[1] / "tests" / "wizard_mock_data.json"
|
||||
with open(mock_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 1. Start with clean payload
|
||||
payload = new_quote_payload()
|
||||
|
||||
# 2. Extract patch from wizard mock data (simulating NLU patch extraction)
|
||||
s1 = data["step1_topic_and_supplier"]
|
||||
s2 = data["step2_line_items"]
|
||||
s3 = data["step3_termsheets"]
|
||||
s4 = data["step4_projects"]
|
||||
s5 = data["step5_review_and_metadata"]
|
||||
|
||||
patch = {
|
||||
"supplier_name": s1["supplier_name"],
|
||||
"is_official": s1["is_official"],
|
||||
"project_names": s4["projects"],
|
||||
"expiry_date": s5["expiry_date"],
|
||||
"notes": s5["notes"],
|
||||
"items": [
|
||||
{
|
||||
"material_name": it["material_name"],
|
||||
"brand_name": it["brand"],
|
||||
"specs": it["specs"],
|
||||
"quantity": it["quantity"],
|
||||
"unit_name": it["unit"],
|
||||
"unit_price": it["unit_price"],
|
||||
}
|
||||
for it in s2
|
||||
],
|
||||
"payment_terms": [
|
||||
{
|
||||
"label": ts["label"],
|
||||
"milestones": ts["milestones"],
|
||||
}
|
||||
for ts in s3
|
||||
],
|
||||
}
|
||||
|
||||
# 3. Merge patch into payload
|
||||
changed = merge_quote_patch(payload, patch)
|
||||
assert changed, "Patch should have changed payload"
|
||||
|
||||
# 4. Simulate resolution
|
||||
payload["supplier_id"] = "11111111-1111-1111-1111-111111111111"
|
||||
payload["project_ids"] = ["22222222-2222-2222-2222-222222222222"]
|
||||
for i, it in enumerate(payload["items"], start=1):
|
||||
it["material_id"] = f"33333333-3333-3333-3333-33333333333{i}"
|
||||
it["unit_id"] = 3
|
||||
|
||||
# 5. Build QuoteCreate request
|
||||
body, problems = build_quote_request(payload)
|
||||
assert problems == [], f"Expected no problems, got {problems}"
|
||||
assert body is not None, "Body should not be None"
|
||||
|
||||
assert body["supplier_id"] == "11111111-1111-1111-1111-111111111111"
|
||||
assert body["is_official"] is True
|
||||
assert len(body["line_items"]) == 2
|
||||
assert body["line_items"][0]["temp_id"] == "t1"
|
||||
assert "A3، سایز ۱۴" in body["line_items"][0]["notes"]
|
||||
assert "فولاد مبارکه" in body["line_items"][0]["notes"]
|
||||
|
||||
assert len(body["termsheets"]) == 1
|
||||
assert body["termsheets"][0]["temp_id"] == "p1"
|
||||
assert len(body["termsheets"][0]["schedule_steps"]) == 3
|
||||
assert body["termsheets"][0]["schedule_steps"][0]["step_no"] == 1
|
||||
assert body["termsheets"][0]["schedule_steps"][0]["amount"] == 300000000.0
|
||||
|
||||
assert len(body["item_termsheets"]) == 2
|
||||
assert body["item_termsheets"][0]["line_item_temp_id"] == "t1"
|
||||
assert body["item_termsheets"][0]["termsheet_temp_id"] == "p1"
|
||||
assert body["item_termsheets"][0]["unit_price"] == 48500.0
|
||||
|
||||
print("ALL WIZARD CONVERSION ASSERTIONS PASSED SUCCESSFULLY! [OK]")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_wizard_conversion()
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"step1_topic_and_supplier": {
|
||||
"topic": "آهنآلات و مقاطع فولادی",
|
||||
"supplier_name": "فولاد آذرخش",
|
||||
"is_official": true,
|
||||
"new_supplier_details": {
|
||||
"location": "تهران - ونک",
|
||||
"phone": "02188776655",
|
||||
"contact_person": "حسینی"
|
||||
}
|
||||
},
|
||||
"step2_line_items": [
|
||||
{
|
||||
"material_name": "میلگرد",
|
||||
"brand": "فولاد مبارکه",
|
||||
"specs": [
|
||||
"A3",
|
||||
"سایز ۱۴"
|
||||
],
|
||||
"quantity": 12500.0,
|
||||
"unit": "کیلوگرم",
|
||||
"unit_price": 48500.0,
|
||||
"total_price": 606250000.0
|
||||
},
|
||||
{
|
||||
"material_name": "میلگرد",
|
||||
"brand": "فولاد مبارکه",
|
||||
"specs": [
|
||||
"A3",
|
||||
"سایز ۱۶"
|
||||
],
|
||||
"quantity": 8000.0,
|
||||
"unit": "کیلوگرم",
|
||||
"unit_price": 47200.0,
|
||||
"total_price": 377600000.0
|
||||
}
|
||||
],
|
||||
"step3_termsheets": [
|
||||
{
|
||||
"label": "گزینه ۱",
|
||||
"milestones": [
|
||||
{
|
||||
"due_date": "2026-09-22",
|
||||
"amount": 300000000.0
|
||||
},
|
||||
{
|
||||
"due_date": "2026-09-28",
|
||||
"amount": 450000000.0
|
||||
},
|
||||
{
|
||||
"due_date": "2026-10-15",
|
||||
"amount": 233850000.0
|
||||
}
|
||||
],
|
||||
"covered_items": [
|
||||
"میلگرد - فولاد مبارکه - A3 - سایز ۱۴",
|
||||
"میلگرد - فولاد مبارکه - A3 - سایز ۱۶"
|
||||
]
|
||||
}
|
||||
],
|
||||
"step4_projects": {
|
||||
"projects": [
|
||||
"برج آسمان"
|
||||
],
|
||||
"item_project_assignments": [
|
||||
{
|
||||
"item_name": "میلگرد - فولاد مبارکه - A3 - سایز ۱۴",
|
||||
"assigned_projects": [
|
||||
"برج آسمان"
|
||||
]
|
||||
},
|
||||
{
|
||||
"item_name": "میلگرد - فولاد مبارکه - A3 - سایز ۱۶",
|
||||
"assigned_projects": [
|
||||
"برج آسمان"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"step5_review_and_metadata": {
|
||||
"quote_date": "2026-09-19T08:30:00Z",
|
||||
"expiry_date": "2026-10-05",
|
||||
"notes": "تحویل پای کارگاه برج آسمان. کرایه حمل با فروشنده است.",
|
||||
"proforma_file": "پیشفاکتور_فولاد_آذرخش_۱۴۰۳.pdf"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"step1_topic_and_supplier": {
|
||||
"topic": null,
|
||||
"supplier_name": null,
|
||||
"is_official": null,
|
||||
"new_supplier_details": {
|
||||
"location": null,
|
||||
"phone": null,
|
||||
"contact_person": null
|
||||
}
|
||||
},
|
||||
"step2_line_items": [
|
||||
{
|
||||
"material_name": null,
|
||||
"brand": null,
|
||||
"specs": [],
|
||||
"quantity": null,
|
||||
"unit": null,
|
||||
"unit_price": null,
|
||||
"total_price": null
|
||||
}
|
||||
],
|
||||
"step3_termsheets": [
|
||||
{
|
||||
"label": null,
|
||||
"milestones": [
|
||||
{
|
||||
"due_date": null,
|
||||
"amount": null
|
||||
}
|
||||
],
|
||||
"covered_items": []
|
||||
}
|
||||
],
|
||||
"step4_projects": {
|
||||
"projects": [],
|
||||
"item_project_assignments": [
|
||||
{
|
||||
"item_name": null,
|
||||
"assigned_projects": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"step5_review_and_metadata": {
|
||||
"quote_date": null,
|
||||
"expiry_date": null,
|
||||
"notes": null,
|
||||
"proforma_file": null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user