PD03 commited on
Commit
4d067a4
·
verified ·
1 Parent(s): a21f844

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +400 -7
app.py CHANGED
@@ -1,13 +1,406 @@
 
1
  import os
2
  import requests
 
3
  import gradio as gr
 
 
 
 
4
 
5
- HF_API_TOKEN = os.getenv("HF_API_TOKEN")
6
- API_URL = "https://api-inference.huggingface.co/models/meta-llama/Meta-Llama-3-8B-Instruct"
7
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
8
 
9
- def call_hf(prompt):
10
- response = requests.post(API_URL, headers=headers, json={"inputs": prompt})
11
- return f"Status: {response.status_code}, Response: {response.text}"
 
12
 
13
- gr.Interface(call_hf, "text", "text").launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setup Hugging Face Transformers for LLAMA3
2
  import os
3
  import requests
4
+ import json
5
  import gradio as gr
6
+ from typing import List, Dict, Any, Optional
7
+ import logging
8
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
9
+ import torch
10
 
11
+ # Setup logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
 
15
+ # Configuration - Set these as environment variables in Hugging Face Spaces
16
+ SAP_API_KEY = os.getenv('SAP_API_KEY') # Set in Space secrets
17
+ HF_TOKEN = os.getenv('HF_API_TOKEN') # Set in Space secrets for private models
18
+ SAP_BASE_URL = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap"
19
 
20
+ # Initialize LLAMA3 model
21
+ MODEL_NAME = "meta-llama/Meta-Llama-3-8B-Instruct" # or "meta-llama/Meta-Llama-3-70B-Instruct" for larger model
22
+
23
+ class LLAMA3Client:
24
+ def __init__(self):
25
+ try:
26
+ # Initialize tokenizer and model
27
+ logger.info("Loading LLAMA3 model...")
28
+ self.tokenizer = AutoTokenizer.from_pretrained(
29
+ MODEL_NAME,
30
+ token=HF_TOKEN,
31
+ trust_remote_code=True
32
+ )
33
+
34
+ # Use GPU if available
35
+ device = "cuda" if torch.cuda.is_available() else "cpu"
36
+ logger.info(f"Using device: {device}")
37
+
38
+ self.model = AutoModelForCausalLM.from_pretrained(
39
+ MODEL_NAME,
40
+ token=HF_TOKEN,
41
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
42
+ device_map="auto" if device == "cuda" else None,
43
+ trust_remote_code=True,
44
+ low_cpu_mem_usage=True
45
+ )
46
+
47
+ # Create text generation pipeline
48
+ self.generator = pipeline(
49
+ "text-generation",
50
+ model=self.model,
51
+ tokenizer=self.tokenizer,
52
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
53
+ device_map="auto" if device == "cuda" else None
54
+ )
55
+
56
+ logger.info("LLAMA3 model loaded successfully")
57
+
58
+ except Exception as e:
59
+ logger.error(f"Error loading LLAMA3 model: {e}")
60
+ # Fallback to smaller model or API-based approach
61
+ try:
62
+ self.generator = pipeline(
63
+ "text-generation",
64
+ model="microsoft/DialoGPT-medium",
65
+ tokenizer="microsoft/DialoGPT-medium"
66
+ )
67
+ logger.info("Fallback model loaded")
68
+ except:
69
+ self.generator = None
70
+ logger.error("Failed to load any model")
71
+
72
+ def generate_response(self, prompt: str, max_length: int = 1000, temperature: float = 0.1) -> str:
73
+ """Generate response using LLAMA3"""
74
+ if not self.generator:
75
+ return "Model not available. Please check configuration."
76
+
77
+ try:
78
+ # Format prompt for LLAMA3 instruction format
79
+ formatted_prompt = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
80
+
81
+ You are a helpful SAP data analyst. Provide clear, concise answers based on the provided data.<|eot_id|><|start_header_id|>user<|end_header_id|>
82
+
83
+ {prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
84
+
85
+ """
86
+
87
+ # Generate response
88
+ outputs = self.generator(
89
+ formatted_prompt,
90
+ max_length=max_length,
91
+ temperature=temperature,
92
+ do_sample=True,
93
+ top_p=0.9,
94
+ num_return_sequences=1,
95
+ pad_token_id=self.tokenizer.eos_token_id,
96
+ eos_token_id=self.tokenizer.eos_token_id
97
+ )
98
+
99
+ # Extract generated text
100
+ generated_text = outputs[0]['generated_text']
101
+
102
+ # Extract only the assistant's response
103
+ if "<|start_header_id|>assistant<|end_header_id|>" in generated_text:
104
+ response = generated_text.split("<|start_header_id|>assistant<|end_header_id|>")[-1]
105
+ response = response.replace("<|eot_id|>", "").strip()
106
+ else:
107
+ response = generated_text[len(formatted_prompt):].strip()
108
+
109
+ return response if response else "I couldn't generate a proper response. Please try rephrasing your question."
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error generating response: {e}")
113
+ return f"I encountered an error while processing your question: {str(e)}"
114
+
115
+ class SAPDataFetcher:
116
+ def __init__(self, api_key: str):
117
+ self.api_key = api_key
118
+ self.headers = {
119
+ "APIKey": api_key,
120
+ "Accept": "application/json",
121
+ "Content-Type": "application/json"
122
+ }
123
+
124
+ def _make_request(self, url: str, timeout: int = 30) -> Optional[Dict]:
125
+ """Make HTTP request with proper error handling"""
126
+ try:
127
+ logger.info(f"Making request to: {url}")
128
+ response = requests.get(url, headers=self.headers, timeout=timeout)
129
+ response.raise_for_status()
130
+ data = response.json()
131
+ logger.info(f"Request successful. Response size: {len(str(data))}")
132
+ return data
133
+ except requests.exceptions.RequestException as e:
134
+ logger.error(f"Request failed: {e}")
135
+ return None
136
+ except json.JSONDecodeError as e:
137
+ logger.error(f"JSON decode error: {e}")
138
+ return None
139
+
140
+ def fetch_sales_orders(self, top: int = 50) -> List[Dict]:
141
+ """Fetch sales orders with error handling"""
142
+ url = f"{SAP_BASE_URL}/API_SALES_ORDER_SRV/A_SalesOrder?$top={top}&$inlinecount=allpages"
143
+ data = self._make_request(url)
144
+
145
+ if data and 'd' in data and 'results' in data['d']:
146
+ orders = data['d']['results']
147
+ # Simplify the data structure
148
+ simplified_orders = []
149
+ for order in orders:
150
+ simplified_order = {
151
+ "SalesOrder": order.get("SalesOrder", ""),
152
+ "SalesOrderType": order.get("SalesOrderType", ""),
153
+ "SalesOrganization": order.get("SalesOrganization", ""),
154
+ "SoldToParty": order.get("SoldToParty", ""),
155
+ "CreationDate": order.get("CreationDate", ""),
156
+ "CreatedByUser": order.get("CreatedByUser", ""),
157
+ "TransactionCurrency": order.get("TransactionCurrency", ""),
158
+ "TotalNetAmount": order.get("TotalNetAmount", "0")
159
+ }
160
+ simplified_orders.append(simplified_order)
161
+ return simplified_orders
162
+ else:
163
+ logger.error("Failed to fetch sales orders or invalid response format")
164
+ return []
165
+
166
+ def fetch_purchase_orders(self, top: int = 50) -> List[Dict]:
167
+ """Fetch purchase order headers"""
168
+ url = f"{SAP_BASE_URL}/API_PURCHASEORDER_PROCESS_SRV/A_PurchaseOrder?$top={top}&$inlinecount=allpages"
169
+ data = self._make_request(url)
170
+
171
+ if data and 'd' in data and 'results' in data['d']:
172
+ orders = data['d']['results']
173
+ simplified_orders = []
174
+ for order in orders:
175
+ simplified_order = {
176
+ "PurchaseOrder": order.get("PurchaseOrder", ""),
177
+ "CompanyCode": order.get("CompanyCode", ""),
178
+ "PurchaseOrderType": order.get("PurchaseOrderType", ""),
179
+ "CreatedByUser": order.get("CreatedByUser", ""),
180
+ "CreationDate": order.get("CreationDate", ""),
181
+ "Supplier": order.get("Supplier", ""),
182
+ "PurchasingOrganization": order.get("PurchasingOrganization", ""),
183
+ "PurchasingGroup": order.get("PurchasingGroup", ""),
184
+ "PurchaseOrderDate": order.get("PurchaseOrderDate", ""),
185
+ "DocumentCurrency": order.get("DocumentCurrency", ""),
186
+ "ExchangeRate": order.get("ExchangeRate", "1.0")
187
+ }
188
+ simplified_orders.append(simplified_order)
189
+ return simplified_orders
190
+ else:
191
+ logger.error("Failed to fetch purchase orders or invalid response format")
192
+ return []
193
+
194
+ def fetch_purchase_order_items(self, purchase_orders: List[str]) -> List[Dict]:
195
+ """Fetch purchase order items for given order numbers"""
196
+ all_items = []
197
+
198
+ for po_number in purchase_orders[:10]: # Limit to first 10 to avoid timeout
199
+ url = f"{SAP_BASE_URL}/API_PURCHASEORDER_PROCESS_SRV/A_PurchaseOrderItem?$filter=PurchaseOrder eq '{po_number}'"
200
+ data = self._make_request(url)
201
+
202
+ if data and 'd' in data and 'results' in data['d']:
203
+ items = data['d']['results']
204
+ for item in items:
205
+ simplified_item = {
206
+ "PurchaseOrder": item.get("PurchaseOrder", ""),
207
+ "PurchaseOrderItem": item.get("PurchaseOrderItem", ""),
208
+ "Plant": item.get("Plant", ""),
209
+ "StorageLocation": item.get("StorageLocation", ""),
210
+ "MaterialGroup": item.get("MaterialGroup", ""),
211
+ "OrderQuantity": item.get("OrderQuantity", "0"),
212
+ "PurchaseOrderQuantityUnit": item.get("PurchaseOrderQuantityUnit", ""),
213
+ "DocumentCurrency": item.get("DocumentCurrency", ""),
214
+ "NetPriceAmount": item.get("NetPriceAmount", "0"),
215
+ "NetPriceQuantity": item.get("NetPriceQuantity", "0")
216
+ }
217
+ all_items.append(simplified_item)
218
+
219
+ return all_items
220
+
221
+ class SAPAgent:
222
+ def __init__(self, data_fetcher: SAPDataFetcher, llama_client: LLAMA3Client):
223
+ self.data_fetcher = data_fetcher
224
+ self.llama_client = llama_client
225
+
226
+ def categorize_query(self, question: str) -> str:
227
+ """Determine if query is about sales or purchase orders"""
228
+ category_prompt = f"""Analyze this question and determine if it's about Sales Orders or Purchase Orders:
229
+
230
+ Question: "{question}"
231
+
232
+ Guidelines:
233
+ - Sales Orders: customer orders, sales transactions, revenue, sold to party
234
+ - Purchase Orders: supplier orders, procurement, purchasing, vendor transactions
235
+
236
+ Respond with exactly one word: "sales" or "purchase" """
237
+
238
+ try:
239
+ response = self.llama_client.generate_response(category_prompt, max_length=20, temperature=0)
240
+ category = response.strip().lower()
241
+ return "sales" if "sales" in category else "purchase"
242
+ except Exception as e:
243
+ logger.error(f"Error in categorization: {e}")
244
+ return "purchase" # Default to purchase
245
+
246
+ def needs_item_details(self, question: str) -> bool:
247
+ """Determine if question requires item-level details"""
248
+ detail_prompt = f"""Does this question require detailed item-level information (quantities, prices, materials, line items)?
249
+
250
+ Question: "{question}"
251
+
252
+ Answer only "yes" or "no" """
253
+
254
+ try:
255
+ response = self.llama_client.generate_response(detail_prompt, max_length=20, temperature=0)
256
+ answer = response.strip().lower()
257
+ return "yes" in answer
258
+ except Exception as e:
259
+ logger.error(f"Error determining detail needs: {e}")
260
+ return False
261
+
262
+ def process_query(self, question: str) -> str:
263
+ """Main function to process user queries"""
264
+ logger.info(f"Processing query: {question}")
265
+
266
+ # Categorize the query
267
+ category = self.categorize_query(question)
268
+ logger.info(f"Query categorized as: {category}")
269
+
270
+ # Fetch appropriate data
271
+ if category == "sales":
272
+ data = self.data_fetcher.fetch_sales_orders()
273
+ data_type = "Sales Orders"
274
+ context = {"orders": data}
275
+ else:
276
+ # Fetch purchase order headers
277
+ po_headers = self.data_fetcher.fetch_purchase_orders()
278
+ context = {"headers": po_headers}
279
+ data_type = "Purchase Order Headers"
280
+
281
+ # Check if item details are needed
282
+ if self.needs_item_details(question) and po_headers:
283
+ logger.info("Fetching item-level details")
284
+ po_numbers = [po["PurchaseOrder"] for po in po_headers if po["PurchaseOrder"]]
285
+ po_items = self.data_fetcher.fetch_purchase_order_items(po_numbers)
286
+ context["items"] = po_items
287
+ data_type = "Purchase Orders with Item Details"
288
+
289
+ # Calculate total value
290
+ total_value = 0.0
291
+ for item in po_items:
292
+ try:
293
+ net_price = float(item.get("NetPriceAmount", 0))
294
+ quantity = float(item.get("OrderQuantity", 0))
295
+ total_value += net_price * quantity
296
+ except (ValueError, TypeError):
297
+ continue
298
+ context["total_value"] = total_value
299
+
300
+ # Generate response using LLAMA3
301
+ return self.generate_response(question, context, data_type)
302
+
303
+ def generate_response(self, question: str, context: Dict, data_type: str) -> str:
304
+ """Generate response using LLAMA3"""
305
+ # Limit context size to prevent token overflow
306
+ context_str = json.dumps(context, indent=2)
307
+ if len(context_str) > 4000: # Smaller limit for LLAMA3
308
+ context_str = context_str[:4000] + "... (truncated)"
309
+
310
+ prompt = f"""Data Type: {data_type}
311
+
312
+ Available Data:
313
+ {context_str}
314
+
315
+ User Question: {question}
316
+
317
+ Instructions:
318
+ 1. Provide a clear, concise answer based on the data
319
+ 2. Include specific numbers, dates, or values when relevant
320
+ 3. If the data doesn't contain enough information to answer fully, mention this
321
+ 4. Format your response in a user-friendly way
322
+ 5. If there are multiple records, summarize key insights"""
323
+
324
+ try:
325
+ return self.llama_client.generate_response(prompt, max_length=800, temperature=0.1)
326
+ except Exception as e:
327
+ logger.error(f"Error generating response: {e}")
328
+ return f"I encountered an error while processing your question: {str(e)}"
329
+
330
+ # Initialize the system
331
+ try:
332
+ llama_client = LLAMA3Client()
333
+ if SAP_API_KEY:
334
+ data_fetcher = SAPDataFetcher(SAP_API_KEY)
335
+ sap_agent = SAPAgent(data_fetcher, llama_client)
336
+ logger.info("SAP Agent initialized successfully")
337
+ else:
338
+ logger.warning("SAP_API_KEY not found. Demo mode enabled.")
339
+ sap_agent = None
340
+ except Exception as e:
341
+ logger.error(f"Failed to initialize SAP Agent: {e}")
342
+ sap_agent = None
343
+
344
+ # Gradio Interface
345
+ def chat_with_sap(message, history):
346
+ """Handle chat interactions"""
347
+ if not sap_agent:
348
+ return history + [("System", "SAP Agent not initialized. Please check your API key configuration in Space secrets.")]
349
+
350
+ if not message.strip():
351
+ return history
352
+
353
+ try:
354
+ response = sap_agent.process_query(message)
355
+ history = history or []
356
+ history.append((message, response))
357
+ return history
358
+ except Exception as e:
359
+ error_msg = f"Error processing your request: {str(e)}"
360
+ history = history or []
361
+ history.append((message, error_msg))
362
+ return history
363
+
364
+ def clear_chat():
365
+ return []
366
+
367
+ # Create Gradio interface
368
+ with gr.Blocks(title="SAP Order Analytics Agent with LLAMA3") as demo:
369
+ gr.Markdown("""
370
+ # 🚀 SAP Order Analytics Agent (Powered by LLAMA3)
371
+
372
+ This AI agent uses Meta's LLAMA3 model to help you analyze SAP Sales and Purchase Orders. Ask questions like:
373
+ - "How many sales orders do we have?"
374
+ - "What's the total value of all purchase orders?"
375
+ - "Show me recent purchase orders from supplier X"
376
+ - "What are the top materials by quantity?"
377
+
378
+ **Note:** Make sure to set your `SAP_API_KEY` and `HF_TOKEN` in the Space secrets.
379
+ """)
380
+
381
+ chatbot = gr.Chatbot(
382
+ height=500,
383
+ placeholder="Ask me anything about your SAP orders..."
384
+ )
385
+
386
+ with gr.Row():
387
+ msg = gr.Textbox(
388
+ label="Your Question",
389
+ placeholder="Type your question here...",
390
+ scale=4
391
+ )
392
+ submit_btn = gr.Button("Send", scale=1, variant="primary")
393
+ clear_btn = gr.Button("Clear", scale=1)
394
+
395
+ # Event handlers
396
+ submit_btn.click(chat_with_sap, [msg, chatbot], [chatbot])
397
+ msg.submit(chat_with_sap, [msg, chatbot], [chatbot])
398
+ clear_btn.click(clear_chat, outputs=[chatbot])
399
+
400
+ # Clear input after submission
401
+ submit_btn.click(lambda: "", outputs=[msg])
402
+ msg.submit(lambda: "", outputs=[msg])
403
+
404
+ # Launch the interface
405
+ if __name__ == "__main__":
406
+ demo.launch()