MakPr016 commited on
Commit
0fc22ba
·
0 Parent(s):

Updated dates

Browse files
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ COPY . /code
10
+
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Vendor Analysis API
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Vendor Selection & Analysis API
12
+
13
+ A high-performance FastAPI service designed to analyze vendor quotations for medical supply RFQs. It uses combinatorial optimization algorithms to recommend the best vendor allocation strategies (Lowest Cost, Fastest Delivery, Best Quality, and Balanced).
14
+
15
+ This API is built to be deployed on **Hugging Face Spaces** using Docker and connects to a **Supabase** backend.
16
+
17
+ ## 🚀 Features
18
+
19
+ * **Multi-Strategy Optimization**: automatically generates four distinct procurement strategies:
20
+ * **Lowest Cost**: Minimizes total spend.
21
+ * **Fastest Delivery**: Prioritizes lead time.
22
+ * **Best Quality**: Maximizes vendor rating scores.
23
+ * **Balanced**: A weighted mix of cost, speed, and quality.
24
+ * **Dynamic Bundle Generation**: Groups items into logical vendor bundles to streamline purchasing.
25
+ * **Supabase Integration**: Fetches real-time RFQ and Quotation data directly from your database.
26
+ * **Dockerized**: Ready for deployment on any container platform (Hugging Face, Railway, AWS).
27
+
28
+ ## 🛠️ Tech Stack
29
+
30
+ * **Framework**: FastAPI (Python 3.9+)
31
+ * **Database**: Supabase (PostgreSQL)
32
+ * **Data Processing**: Pandas, NumPy
33
+ * **Server**: Uvicorn
34
+ * **Deployment**: Docker
35
+
36
+ ## 📂 Project Structure
37
+
38
+ ```text
39
+ .
40
+ ├── main.py # API Entry point & Routes
41
+ ├── models.py # Pydantic models & Data structures
42
+ ├── optimizer.py # Core logic for vendor selection algorithms
43
+ ├── requirements.txt # Python dependencies
44
+ ├── Dockerfile # Docker configuration for Hugging Face
45
+ └── README.md # Documentation
46
+ ```
47
+
48
+ ## ⚡ Local Development
49
+
50
+ ### 1. Clone the repository
51
+
52
+ ```bash
53
+ git clone [https://github.com/your-username/vendor-analysis-api.git](https://github.com/your-username/vendor-analysis-api.git)
54
+ cd vendor-analysis-api
55
+ ```
56
+
57
+ ### 2. Create a Virtual Environment
58
+
59
+ ```bash
60
+ python -m venv venv
61
+ source venv/bin/activate # On Windows: venv\Scripts\activate
62
+
63
+ ```
64
+
65
+ ### 3. Install Dependencies
66
+
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ ```
70
+
71
+ ### 4. Environment Configuration
72
+
73
+ Create a `.env` file in the root directory with your Supabase credentials:
74
+
75
+ ```env
76
+ SUPABASE_URL=your_supabase_url
77
+ SUPABASE_SERVICE_KEY=your_supabase_service_key
78
+ ```
79
+
80
+ ### 5. Run the Server
81
+
82
+ ```bash
83
+ uvicorn main:app --reload --port 8000
84
+ ```
85
+
86
+ The API will be available at `http://localhost:8000`.
87
+
88
+ ## 🚀 API Endpoints
89
+
90
+ ### Analyze RFQ
91
+
92
+ **GET** `/api/analyze/{rfq_id}`
93
+
94
+ Analyzes a specific RFQ and returns optimal vendor allocation strategies.
95
+
96
+ **Parameters:**
97
+
98
+ * `rfq_id` (path): The ID of the RFQ to analyze.
99
+
100
+ **Response:**
101
+
102
+ ```json
103
+ {
104
+ "rfq_id": "rfq-123",
105
+ "recommended_strategy": "balanced",
106
+ "strategies": {
107
+ "lowest_cost": {
108
+ "total_cost": 1250.00,
109
+ "total_delivery_days": 15,
110
+ "savings": 250.00,
111
+ "bundles": [
112
+ {
113
+ "vendor_id": "vendor-abc",
114
+ "vendor_name": "MediCorp Supplies",
115
+ "items": ["item-001", "item-003"],
116
+ "cost": 800.00,
117
+ "delivery_days": 10
118
+ }
119
+ ]
120
+ },
121
+ "fastest_delivery": {
122
+ "total_cost": 1300.00,
123
+ "total_delivery_days": 8,
124
+ "savings": 200.00,
125
+ "bundles": [
126
+ {
127
+ "vendor_id": "vendor-xyz",
128
+ "vendor_name": "QuickMed Inc",
129
+ "items": ["item-001", "item-002"],
130
+ "cost": 1300.00,
131
+ "delivery_days": 8
132
+ }
133
+ ]
134
+ },
135
+ "best_quality": {
136
+ "total_cost": 1400.00,
137
+ "total_delivery_days": 12,
138
+ "savings": 100.00,
139
+ "bundles": [
140
+ {
141
+ "vendor_id": "vendor-def",
142
+ "vendor_name": "Premium Health",
143
+ "items": ["item-001", "item-002", "item-003"],
144
+ "cost": 1400.00,
145
+ "delivery_days": 12
146
+ }
147
+ ]
148
+ },
149
+ "balanced": {
150
+ "total_cost": 1350.00,
151
+ "total_delivery_days": 10,
152
+ "savings": 150.00,
153
+ "bundles": [
154
+ {
155
+ "vendor_id": "vendor-abc",
156
+ "vendor_name": "MediCorp Supplies",
157
+ "items": ["item-001"],
158
+ "cost": 500.00,
159
+ "delivery_days": 10
160
+ },
161
+ {
162
+ "vendor_id": "vendor-xyz",
163
+ "vendor_name": "QuickMed Inc",
164
+ "items": ["item-002", "item-003"],
165
+ "cost": 850.00,
166
+ "delivery_days": 10
167
+ }
168
+ ]
169
+ }
170
+ }
171
+ }
172
+ ```
__pycache__/models.cpython-310.pyc ADDED
Binary file (1.99 kB). View file
 
__pycache__/optimizer.cpython-310.pyc ADDED
Binary file (4.21 kB). View file
 
main.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from supabase import create_client, Client
6
+ from dotenv import load_dotenv
7
+ from models import AnalysisResponse, RFQItem, VendorOffer, QuoteOffer
8
+ from optimizer import VendorOptimizer
9
+
10
+ load_dotenv()
11
+
12
+ url: str = os.environ.get("SUPABASE_URL")
13
+ key: str = os.environ.get("SUPABASE_SERVICE_KEY")
14
+ supabase: Client = create_client(url, key)
15
+
16
+ app = FastAPI()
17
+
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ @app.get("/api/analyze/{rfq_id}", response_model=AnalysisResponse)
26
+ async def analyze_rfq(rfq_id: str):
27
+ items_res = supabase.table('rfq_line_items').select("*").eq('rfq_id', rfq_id).execute()
28
+ if not items_res.data:
29
+ raise HTTPException(status_code=404, detail="RFQ Items not found")
30
+
31
+ rfq_items = [
32
+ RFQItem(id=item['id'], inn_name=item['inn_name'], quantity=item['quantity'])
33
+ for item in items_res.data
34
+ ]
35
+
36
+ quotes_res = supabase.table('quotations').select(
37
+ "id, vendor_id, vendors(vendor_name, rating, is_verified)"
38
+ ).eq('rfq_id', rfq_id).execute()
39
+
40
+ quote_ids = [q['id'] for q in quotes_res.data]
41
+ if not quote_ids:
42
+ return AnalysisResponse(rfq_id=rfq_id, strategies={}, recommended_strategy="None")
43
+
44
+ q_items_res = supabase.table('quotation_items').select(
45
+ "rfq_item_id, unit_price, delivery_time_days, quotation_id, brand, manufacturer"
46
+ ).in_('quotation_id', quote_ids).execute()
47
+
48
+ vendor_map = {}
49
+
50
+ for q in quotes_res.data:
51
+ v_id = q['vendor_id']
52
+ if v_id not in vendor_map:
53
+ vendor_data = q['vendors']
54
+ vendor_map[v_id] = VendorOffer(
55
+ vendor_id=v_id,
56
+ vendor_name=vendor_data.get('vendor_name', 'Unknown') or 'Unknown',
57
+ rating=float(vendor_data.get('rating', 0) or 0),
58
+ is_verified=vendor_data.get('is_verified', False),
59
+ offers=[]
60
+ )
61
+
62
+ q_to_v = {q['id']: q['vendor_id'] for q in quotes_res.data}
63
+
64
+ for qi in q_items_res.data:
65
+ q_id = qi['quotation_id']
66
+ if q_id in q_to_v:
67
+ v_id = q_to_v[q_id]
68
+ vendor_map[v_id].offers.append(QuoteOffer(
69
+ rfq_item_id=qi['rfq_item_id'],
70
+ unit_price=qi['unit_price'],
71
+ delivery_days=qi.get('delivery_time_days', 7),
72
+ brand=qi.get('brand'),
73
+ manufacturer=qi.get('manufacturer')
74
+ ))
75
+
76
+ vendor_offers = list(vendor_map.values())
77
+
78
+ optimizer = VendorOptimizer(rfq_items, vendor_offers)
79
+ results = optimizer.run_all()
80
+
81
+ rec = "balanced"
82
+ if "lowest_cost" in results and "balanced" in results:
83
+ if results['lowest_cost'].savings > results['balanced'].savings * 1.5:
84
+ rec = "lowest_cost"
85
+ elif results['fastest_delivery'].avg_delivery_days < results['balanced'].avg_delivery_days - 2:
86
+ rec = "fastest_delivery"
87
+
88
+ return AnalysisResponse(
89
+ rfq_id=rfq_id,
90
+ strategies=results,
91
+ recommended_strategy=rec
92
+ )
93
+
94
+ if __name__ == "__main__":
95
+ uvicorn.run(app, host="0.0.0.0", port=8000)
models.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional, Dict
3
+
4
+ class SelectedVendor(BaseModel):
5
+ vendor_id: str
6
+ vendor_name: str
7
+ total_cost: float
8
+ items_count: int
9
+ item_ids: List[str]
10
+ avg_quality_score: float
11
+
12
+ class StrategyResult(BaseModel):
13
+ strategy_name: str
14
+ total_cost: float
15
+ avg_delivery_days: float
16
+ vendor_count: int
17
+ savings: float
18
+ score: float
19
+ quality_score: float
20
+ allocations: List[SelectedVendor]
21
+
22
+ class AnalysisResponse(BaseModel):
23
+ rfq_id: str
24
+ strategies: Dict[str, StrategyResult]
25
+ recommended_strategy: str
26
+
27
+ class RFQItem(BaseModel):
28
+ id: str
29
+ inn_name: str
30
+ quantity: int
31
+
32
+ class QuoteOffer(BaseModel):
33
+ rfq_item_id: str
34
+ unit_price: float
35
+ delivery_days: int
36
+ brand: Optional[str] = None
37
+ manufacturer: Optional[str] = None
38
+
39
+ class VendorOffer(BaseModel):
40
+ vendor_id: str
41
+ vendor_name: str
42
+ rating: float
43
+ is_verified: bool
44
+ offers: List[QuoteOffer]
optimizer.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+ from models import StrategyResult, SelectedVendor, RFQItem, VendorOffer
3
+
4
+ class VendorOptimizer:
5
+ def __init__(self, rfq_items: List[RFQItem], vendor_offers: List[VendorOffer]):
6
+ self.rfq_items = {item.id: item for item in rfq_items}
7
+ self.vendors = vendor_offers
8
+
9
+ def _calculate_item_quality(self, offer: dict) -> float:
10
+ score = 5.0
11
+ trust_bonus = (offer['rating'] / 5.0) * 5.0
12
+ score += trust_bonus
13
+ return min(round(score, 1), 10.0)
14
+
15
+ def _calculate_strategy(self, strategy_name: str) -> StrategyResult:
16
+ item_offers = {i_id: [] for i_id in self.rfq_items}
17
+
18
+ for vendor in self.vendors:
19
+ for offer in vendor.offers:
20
+ if offer.rfq_item_id in item_offers:
21
+ offer_data = {
22
+ "vendor_id": vendor.vendor_id,
23
+ "vendor_name": vendor.vendor_name,
24
+ "price": offer.unit_price,
25
+ "delivery": offer.delivery_days,
26
+ "rating": vendor.rating,
27
+ "brand": offer.brand,
28
+ "manufacturer": offer.manufacturer
29
+ }
30
+ offer_data['quality_score'] = self._calculate_item_quality(offer_data)
31
+ item_offers[offer.rfq_item_id].append(offer_data)
32
+
33
+ selected_map = {}
34
+ total_rfq_cost = 0.0
35
+ total_delivery_days = 0
36
+ total_quality_accum = 0.0
37
+ fulfilled_count = 0
38
+ total_market_cost = 0.0
39
+
40
+ for item_id, item_req in self.rfq_items.items():
41
+ offers = item_offers.get(item_id, [])
42
+ if not offers:
43
+ continue
44
+
45
+ max_price_for_item = max(o['price'] for o in offers)
46
+ total_market_cost += (max_price_for_item * item_req.quantity)
47
+
48
+ if strategy_name == "lowest_cost":
49
+ offers.sort(key=lambda x: (x['price'], -x['quality_score']))
50
+
51
+ elif strategy_name == "fastest_delivery":
52
+ offers.sort(key=lambda x: (x['delivery'], x['price']))
53
+
54
+ elif strategy_name == "best_quality":
55
+ offers.sort(key=lambda x: (-x['quality_score'], x['price']))
56
+
57
+ elif strategy_name == "balanced":
58
+ offers.sort(key=lambda x: (
59
+ x['price'] * 0.6 +
60
+ x['delivery'] * 2.0 +
61
+ (10.0 - x['quality_score']) * 15.0
62
+ ))
63
+
64
+ best = offers[0]
65
+ vid = best['vendor_id']
66
+
67
+ if vid not in selected_map:
68
+ selected_map[vid] = {
69
+ "vendor_id": vid,
70
+ "vendor_name": best['vendor_name'],
71
+ "total_cost": 0.0,
72
+ "item_ids": [],
73
+ "quality_sum": 0.0
74
+ }
75
+
76
+ line_cost = best['price'] * item_req.quantity
77
+ selected_map[vid]["total_cost"] += line_cost
78
+ selected_map[vid]["item_ids"].append(item_id)
79
+ selected_map[vid]["quality_sum"] += best['quality_score']
80
+
81
+ total_rfq_cost += line_cost
82
+ total_delivery_days += best['delivery']
83
+ total_quality_accum += best['quality_score']
84
+ fulfilled_count += 1
85
+
86
+ allocations = []
87
+ for vid, data in selected_map.items():
88
+ v_avg_qual = data["quality_sum"] / len(data["item_ids"]) if data["item_ids"] else 0
89
+
90
+ allocations.append(SelectedVendor(
91
+ vendor_id=data['vendor_id'],
92
+ vendor_name=data['vendor_name'],
93
+ total_cost=round(data['total_cost'], 2),
94
+ items_count=len(data['item_ids']),
95
+ item_ids=data['item_ids'],
96
+ avg_quality_score=round(v_avg_qual, 1)
97
+ ))
98
+
99
+ avg_delivery = round(total_delivery_days / fulfilled_count, 1) if fulfilled_count else 0
100
+ overall_quality = round(total_quality_accum / fulfilled_count, 1) if fulfilled_count else 0
101
+ savings = round(total_market_cost - total_rfq_cost, 2)
102
+
103
+ ui_score = 70.0
104
+ ui_score += (overall_quality - 5.0) * 6.0
105
+
106
+ if total_market_cost > 0:
107
+ savings_pct = (savings / total_market_cost)
108
+ ui_score += (savings_pct * 100)
109
+
110
+ return StrategyResult(
111
+ strategy_name=strategy_name,
112
+ total_cost=round(total_rfq_cost, 2),
113
+ avg_delivery_days=avg_delivery,
114
+ vendor_count=len(allocations),
115
+ allocations=allocations,
116
+ savings=savings,
117
+ score=max(min(round(ui_score, 1), 100), 40),
118
+ quality_score=overall_quality
119
+ )
120
+
121
+ def run_all(self):
122
+ return {
123
+ "lowest_cost": self._calculate_strategy("lowest_cost"),
124
+ "fastest_delivery": self._calculate_strategy("fastest_delivery"),
125
+ "best_quality": self._calculate_strategy("best_quality"),
126
+ "balanced": self._calculate_strategy("balanced"),
127
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ supabase
4
+ pandas
5
+ numpy
6
+ pydantic
7
+ python-dotenv