barath19 commited on
Commit
209fde9
Β·
1 Parent(s): 8f6480f

fix mcp with true

Browse files
Files changed (2) hide show
  1. app.py +252 -378
  2. requirements.txt +1 -4
app.py CHANGED
@@ -220,48 +220,53 @@ class VDIHeatPumpParser:
220
 
221
  os.makedirs(download_dir, exist_ok=True)
222
 
223
- # Get catalog information
224
- catalog_xml = self.check_catalogs_for_part(part)
225
- root = ET.fromstring(catalog_xml)
226
- result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult')
227
-
228
- if result is None:
229
- raise Exception("No catalog results found")
230
-
231
- catalogs_xml = ET.fromstring(result.text)
232
-
233
- # Extract catalog information
234
- data = []
235
- for catalog in catalogs_xml.findall('Catalog'):
236
- data.append({
237
- 'mfr': catalog.get('mfr'),
238
- 'nick': catalog.get('nick').split('/')[0].strip(),
239
- 'part': f"{int(catalog.get('part')):02d}",
240
- })
241
-
242
- df = pd.DataFrame(data)
243
- df['slug'] = df['nick'] + df['part']
244
- df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip"
245
-
246
- # Download files
247
- downloaded_files = []
248
- for _, row in df.iterrows():
249
- download_url = row['download_url']
250
- file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip")
251
 
252
- try:
253
- response = requests.get(download_url, timeout=30)
254
- response.raise_for_status()
255
-
256
- with open(file_name, 'wb') as file:
257
- file.write(response.content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- downloaded_files.append(file_name)
260
- logger.info(f"Downloaded: {file_name}")
261
- except Exception as e:
262
- logger.error(f"Failed to download {download_url}: {e}")
263
-
264
- return downloaded_files
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame:
267
  """Parse a single VDI file"""
@@ -370,7 +375,8 @@ class VDIHeatPumpParser:
370
  return df
371
 
372
  except Exception as e:
373
- raise Exception(f"Error parsing VDI file {file_path}: {str(e)}")
 
374
 
375
  def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame:
376
  """Extract ZIP file and parse VDI files"""
@@ -385,7 +391,8 @@ class VDIHeatPumpParser:
385
  vdi_files = list(Path(temp_dir).rglob("*.VDI"))
386
 
387
  if not vdi_files:
388
- raise Exception(f"No VDI files found in {zip_path}")
 
389
 
390
  all_data = []
391
  for vdi_file in vdi_files:
@@ -407,8 +414,7 @@ class VDIHeatPumpParser:
407
  # Initialize the parser
408
  parser = VDIHeatPumpParser()
409
 
410
- # Gradio functions (these will become MCP tools)
411
-
412
  def download_vdi_files(part: str, download_dir: str = "") -> str:
413
  """
414
  Download VDI files for a specific part number.
@@ -416,11 +422,13 @@ def download_vdi_files(part: str, download_dir: str = "") -> str:
416
  Args:
417
  part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
418
  download_dir (str): Directory to save downloaded files (optional)
 
 
 
419
  """
420
  try:
421
  part_int = int(part)
422
  download_dir = download_dir.strip() if download_dir.strip() else None
423
-
424
  downloaded_files = parser.download_vdi_files(part_int, download_dir)
425
 
426
  result = {
@@ -431,7 +439,6 @@ def download_vdi_files(part: str, download_dir: str = "") -> str:
431
  "download_directory": download_dir or f"temp directory for part {part_int}"
432
  }
433
  return json.dumps(result, indent=2)
434
-
435
  except Exception as e:
436
  return f"Error downloading VDI files: {str(e)}"
437
 
@@ -442,6 +449,9 @@ def download_and_parse_part(part: str, download_dir: str = "") -> str:
442
  Args:
443
  part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
444
  download_dir (str): Directory to save downloaded files (optional)
 
 
 
445
  """
446
  try:
447
  part_int = int(part)
@@ -450,6 +460,9 @@ def download_and_parse_part(part: str, download_dir: str = "") -> str:
450
  # Download files first
451
  downloaded_files = parser.download_vdi_files(part_int, download_dir)
452
 
 
 
 
453
  # Parse all downloaded files
454
  all_data = []
455
  for file_path in downloaded_files:
@@ -470,19 +483,18 @@ def download_and_parse_part(part: str, download_dir: str = "") -> str:
470
  "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part_int}",
471
  "part": part_int,
472
  "record_count": len(combined_df),
473
- "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist()),
474
  "download_directory": download_dir or f"temp directory for part {part_int}"
475
  }
476
  else:
477
  result = {
478
  "status": "warning",
479
- "message": f"No heat pump data found for part {part_int}",
480
  "part": part_int,
481
  "record_count": 0
482
  }
483
 
484
  return json.dumps(result, indent=2)
485
-
486
  except Exception as e:
487
  return f"Error downloading and parsing part {part}: {str(e)}"
488
 
@@ -492,6 +504,9 @@ def parse_vdi_file(file_path: str) -> str:
492
 
493
  Args:
494
  file_path (str): Path to the VDI ZIP file
 
 
 
495
  """
496
  try:
497
  if not os.path.exists(file_path):
@@ -506,12 +521,17 @@ def parse_vdi_file(file_path: str) -> str:
506
  "record_count": len(df)
507
  }
508
  return json.dumps(result, indent=2)
509
-
510
  except Exception as e:
511
  return f"Error parsing file: {str(e)}"
512
 
513
- def search_heatpump(manufacturer: str = "", product_name: str = "", article_number: str = "",
514
- heating_power_min: str = "", heating_power_max: str = "", type_filter: str = "") -> str:
 
 
 
 
 
 
515
  """
516
  Search for specific heat pump by criteria.
517
 
@@ -521,7 +541,10 @@ def search_heatpump(manufacturer: str = "", product_name: str = "", article_numb
521
  article_number (str): Article number (optional)
522
  heating_power_min (str): Minimum heating power in kW (optional)
523
  heating_power_max (str): Maximum heating power in kW (optional)
524
- type_filter (str): Heat pump type (e.g., 'Luft-Wasser') (optional)
 
 
 
525
  """
526
  try:
527
  # Combine all parsed data
@@ -530,51 +553,62 @@ def search_heatpump(manufacturer: str = "", product_name: str = "", article_numb
530
  all_data.append(df)
531
 
532
  if not all_data:
533
- return "No data available. Please parse VDI files first."
534
 
535
  combined_df = pd.concat(all_data, ignore_index=True)
536
  filtered_df = combined_df.copy()
537
 
538
  # Apply filters
539
  if manufacturer.strip():
540
- filtered_df = filtered_df[
541
- filtered_df['hersteller'].str.contains(manufacturer.strip(), case=False, na=False)
542
- ]
 
543
 
544
  if product_name.strip():
545
- filtered_df = filtered_df[
546
- filtered_df['produktname'].str.contains(product_name.strip(), case=False, na=False)
547
- ]
 
548
 
549
  if article_number.strip():
550
- filtered_df = filtered_df[
551
- filtered_df['artikelnummer'].str.contains(article_number.strip(), case=False, na=False)
552
- ]
 
553
 
554
- if type_filter.strip():
555
- filtered_df = filtered_df[
556
- filtered_df['typ'].str.contains(type_filter.strip(), case=False, na=False)
557
- ]
 
558
 
559
  # Filter by heating power
560
  if heating_power_min.strip() or heating_power_max.strip():
561
- filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce')
562
-
563
- if heating_power_min.strip():
564
- min_power = float(heating_power_min.strip())
565
- filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= min_power]
566
-
567
- if heating_power_max.strip():
568
- max_power = float(heating_power_max.strip())
569
- filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= max_power]
 
570
 
571
  # Return results
572
  result_columns = ['hersteller', 'produktname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse']
573
  available_columns = [col for col in result_columns if col in filtered_df.columns]
 
 
 
 
574
  result_df = filtered_df[available_columns].head(20) # Limit to 20 results
575
 
576
- return result_df.to_json(orient='records', indent=2)
 
577
 
 
578
  except Exception as e:
579
  return f"Error searching heat pumps: {str(e)}"
580
 
@@ -584,25 +618,31 @@ def get_heatpump_details(article_number: str) -> str:
584
 
585
  Args:
586
  article_number (str): Article number of the heat pump
 
 
 
587
  """
588
  try:
589
  # Search across all parsed data
590
  for df in parser.parsed_files.values():
591
- matching_rows = df[df['artikelnummer'] == article_number.strip()]
592
- if not matching_rows.empty:
593
- details = matching_rows.iloc[0].to_dict()
594
- # Clean up None values
595
- details = {k: v for k, v in details.items() if v is not None and v != ''}
596
- return json.dumps(details, indent=2, default=str)
 
597
 
598
  return f"Heat pump with article number '{article_number}' not found"
599
-
600
  except Exception as e:
601
  return f"Error getting heat pump details: {str(e)}"
602
 
603
  def list_manufacturers() -> str:
604
  """
605
  List all available manufacturers in parsed data.
 
 
 
606
  """
607
  try:
608
  all_manufacturers = set()
@@ -612,20 +652,22 @@ def list_manufacturers() -> str:
612
  all_manufacturers.update(manufacturers)
613
 
614
  if not all_manufacturers:
615
- return "No manufacturers available in the parsed data. Please parse VDI files first."
616
 
617
  result = {
618
  "manufacturers": sorted(list(all_manufacturers)),
619
  "count": len(all_manufacturers)
620
  }
621
  return json.dumps(result, indent=2)
622
-
623
  except Exception as e:
624
  return f"Error listing manufacturers: {str(e)}"
625
 
626
  def check_storage() -> str:
627
  """
628
  Check available storage locations and permissions.
 
 
 
629
  """
630
  try:
631
  import tempfile
@@ -656,193 +698,43 @@ def check_storage() -> str:
656
  }
657
 
658
  return json.dumps(result, indent=2)
659
-
660
  except Exception as e:
661
  return f"Error checking storage: {str(e)}"
662
 
663
- # Create MCP tool definitions
664
- MCP_TOOLS = [
665
- {
666
- "name": "download_vdi_files",
667
- "description": "Download VDI files for a specific part number",
668
- "inputSchema": {
669
- "type": "object",
670
- "properties": {
671
- "part": {
672
- "type": "integer",
673
- "description": "Part number (e.g., 2 for heat generators, 22 for heat pumps)"
674
- },
675
- "download_dir": {
676
- "type": "string",
677
- "description": "Directory to save downloaded files (optional)"
678
- }
679
- },
680
- "required": ["part"]
681
- }
682
- },
683
- {
684
- "name": "download_and_parse_part",
685
- "description": "Download and automatically parse all VDI files for a part",
686
- "inputSchema": {
687
- "type": "object",
688
- "properties": {
689
- "part": {
690
- "type": "integer",
691
- "description": "Part number (e.g., 2 for heat generators, 22 for heat pumps)"
692
- },
693
- "download_dir": {
694
- "type": "string",
695
- "description": "Directory to save downloaded files (optional)"
696
- }
697
- },
698
- "required": ["part"]
699
- }
700
- },
701
- {
702
- "name": "parse_vdi_file",
703
- "description": "Parse a VDI ZIP file and extract heat pump data",
704
- "inputSchema": {
705
- "type": "object",
706
- "properties": {
707
- "file_path": {
708
- "type": "string",
709
- "description": "Path to the VDI ZIP file"
710
- }
711
- },
712
- "required": ["file_path"]
713
- }
714
- },
715
- {
716
- "name": "search_heatpump",
717
- "description": "Search for specific heat pump by criteria",
718
- "inputSchema": {
719
- "type": "object",
720
- "properties": {
721
- "manufacturer": {
722
- "type": "string",
723
- "description": "Manufacturer name (optional)"
724
- },
725
- "product_name": {
726
- "type": "string",
727
- "description": "Product name or partial match (optional)"
728
- },
729
- "article_number": {
730
- "type": "string",
731
- "description": "Article number (optional)"
732
- },
733
- "type": {
734
- "type": "string",
735
- "description": "Heat pump type (e.g., 'Luft-Wasser') (optional)"
736
- },
737
- "heating_power_min": {
738
- "type": "number",
739
- "description": "Minimum heating power in kW (optional)"
740
- },
741
- "heating_power_max": {
742
- "type": "number",
743
- "description": "Maximum heating power in kW (optional)"
744
- }
745
- }
746
- }
747
- },
748
- {
749
- "name": "get_heatpump_details",
750
- "description": "Get detailed information about a specific heat pump",
751
- "inputSchema": {
752
- "type": "object",
753
- "properties": {
754
- "article_number": {
755
- "type": "string",
756
- "description": "Article number of the heat pump"
757
- }
758
- },
759
- "required": ["article_number"]
760
- }
761
- },
762
- {
763
- "name": "list_manufacturers",
764
- "description": "List all available manufacturers in parsed data",
765
- "inputSchema": {
766
- "type": "object",
767
- "properties": {}
768
- }
769
- },
770
- {
771
- "name": "check_storage",
772
- "description": "Check available storage locations and permissions",
773
- "inputSchema": {
774
- "type": "object",
775
- "properties": {}
776
- }
777
- }
778
- ]
779
-
780
- # MCP tool router
781
- def route_mcp_tool(tool_name: str, arguments: dict) -> str:
782
- """Route MCP tool calls to the appropriate functions"""
783
- try:
784
- if tool_name == "download_vdi_files":
785
- return download_vdi_files(
786
- str(arguments.get("part", "")),
787
- arguments.get("download_dir", "")
788
- )
789
- elif tool_name == "download_and_parse_part":
790
- return download_and_parse_part(
791
- str(arguments.get("part", "")),
792
- arguments.get("download_dir", "")
793
- )
794
- elif tool_name == "parse_vdi_file":
795
- return parse_vdi_file(arguments.get("file_path", ""))
796
- elif tool_name == "search_heatpump":
797
- return search_heatpump(
798
- arguments.get("manufacturer", ""),
799
- arguments.get("product_name", ""),
800
- arguments.get("article_number", ""),
801
- str(arguments.get("heating_power_min", "")),
802
- str(arguments.get("heating_power_max", "")),
803
- arguments.get("type", "")
804
- )
805
- elif tool_name == "get_heatpump_details":
806
- return get_heatpump_details(arguments.get("article_number", ""))
807
- elif tool_name == "list_manufacturers":
808
- return list_manufacturers()
809
- elif tool_name == "check_storage":
810
- return check_storage()
811
- else:
812
- return f"Unknown tool: {tool_name}"
813
- except Exception as e:
814
- return f"Error executing tool {tool_name}: {str(e)}"
815
-
816
- # Create the Gradio interface with multiple tabs for different functions
817
- with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
818
- gr.Markdown("# VDI Heat Pump Data Parser")
819
- gr.Markdown("Download, parse, and query VDI heat pump specification files")
820
 
821
  with gr.Tabs():
822
  # Tab 1: Download and Parse
823
- with gr.TabItem("Download & Parse"):
824
  gr.Markdown("## Download and Parse VDI Files")
 
825
 
826
  with gr.Row():
827
  with gr.Column():
828
  part_input = gr.Textbox(
829
  label="Part Number",
830
  placeholder="Enter part number (e.g., 22 for heat pumps)",
831
- value="22"
 
832
  )
833
  download_dir_input = gr.Textbox(
834
  label="Download Directory (optional)",
835
- placeholder="Leave empty for automatic temp directory"
 
836
  )
837
 
838
  with gr.Column():
839
- download_btn = gr.Button("Download Files Only", variant="secondary")
840
- parse_btn = gr.Button("Download & Parse", variant="primary")
841
 
842
  download_output = gr.Textbox(
843
  label="Results",
844
- lines=10,
845
- interactive=False
 
846
  )
847
 
848
  download_btn.click(
@@ -858,26 +750,52 @@ with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
858
  )
859
 
860
  # Tab 2: Search Heat Pumps
861
- with gr.TabItem("Search Heat Pumps"):
862
  gr.Markdown("## Search for Heat Pumps")
 
863
 
864
  with gr.Row():
865
  with gr.Column():
866
- manufacturer_input = gr.Textbox(label="Manufacturer", placeholder="e.g., Viessmann")
867
- product_name_input = gr.Textbox(label="Product Name", placeholder="e.g., Vitocal")
868
- article_number_input = gr.Textbox(label="Article Number", placeholder="Exact article number")
 
 
 
 
 
 
 
 
 
 
 
 
869
 
870
  with gr.Column():
871
- type_input = gr.Textbox(label="Heat Pump Type", placeholder="e.g., Luft-Wasser")
872
- min_power_input = gr.Textbox(label="Min Heating Power (kW)", placeholder="e.g., 5")
873
- max_power_input = gr.Textbox(label="Max Heating Power (kW)", placeholder="e.g., 20")
 
 
 
 
 
 
 
 
 
 
 
 
874
 
875
- search_btn = gr.Button("Search Heat Pumps", variant="primary")
876
 
877
  search_output = gr.Textbox(
878
  label="Search Results",
879
- lines=15,
880
- interactive=False
 
881
  )
882
 
883
  search_btn.click(
@@ -888,20 +806,23 @@ with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
888
  )
889
 
890
  # Tab 3: Heat Pump Details
891
- with gr.TabItem("Heat Pump Details"):
892
  gr.Markdown("## Get Detailed Information")
 
893
 
894
  article_detail_input = gr.Textbox(
895
  label="Article Number",
896
- placeholder="Enter exact article number"
 
897
  )
898
 
899
- detail_btn = gr.Button("Get Details", variant="primary")
900
 
901
  detail_output = gr.Textbox(
902
  label="Heat Pump Details",
903
- lines=20,
904
- interactive=False
 
905
  )
906
 
907
  detail_btn.click(
@@ -911,17 +832,24 @@ with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
911
  )
912
 
913
  # Tab 4: Utilities
914
- with gr.TabItem("Utilities"):
915
  gr.Markdown("## Utility Functions")
 
916
 
917
  with gr.Row():
918
- manufacturers_btn = gr.Button("List Manufacturers", variant="secondary")
919
- storage_btn = gr.Button("Check Storage", variant="secondary")
 
 
 
 
 
920
 
921
  utility_output = gr.Textbox(
922
  label="Results",
923
- lines=10,
924
- interactive=False
 
925
  )
926
 
927
  manufacturers_btn.click(
@@ -935,20 +863,23 @@ with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
935
  )
936
 
937
  # Tab 5: Parse Local File
938
- with gr.TabItem("Parse Local File"):
939
  gr.Markdown("## Parse Local VDI ZIP File")
 
940
 
941
  file_path_input = gr.Textbox(
942
  label="File Path",
943
- placeholder="Enter full path to VDI ZIP file"
 
944
  )
945
 
946
- parse_file_btn = gr.Button("Parse File", variant="primary")
947
 
948
  parse_file_output = gr.Textbox(
949
  label="Parse Results",
950
- lines=10,
951
- interactive=False
 
952
  )
953
 
954
  parse_file_btn.click(
@@ -956,115 +887,58 @@ with gr.Blocks(title="VDI Heat Pump Parser", theme=gr.themes.Default()) as demo:
956
  inputs=file_path_input,
957
  outputs=parse_file_output
958
  )
959
-
960
- # Add MCP endpoints to the Gradio app
961
- @demo.additional_routes()
962
- def add_mcp_routes():
963
- from fastapi import Request
964
- from fastapi.responses import JSONResponse, StreamingResponse
965
- import asyncio
966
- from datetime import datetime
967
-
968
- @demo.app.get("/gradio_api/mcp/sse")
969
- async def mcp_sse():
970
- """MCP Server-Sent Events endpoint"""
971
 
972
- async def event_stream():
973
- # Send initial connection event
974
- yield f"data: {json.dumps({'type': 'connection', 'status': 'connected'})}\n\n"
975
-
976
- # Keep connection alive
977
- while True:
978
- await asyncio.sleep(30) # Send heartbeat every 30 seconds
979
- yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': datetime.now().isoformat()})}\n\n"
980
-
981
- return StreamingResponse(
982
- event_stream(),
983
- media_type="text/event-stream",
984
- headers={
985
- "Cache-Control": "no-cache",
986
- "Connection": "keep-alive",
987
- "Access-Control-Allow-Origin": "*",
988
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
989
- "Access-Control-Allow-Headers": "*"
990
- }
991
- )
992
-
993
- @demo.app.post("/gradio_api/mcp/rpc")
994
- async def mcp_rpc(request: Request):
995
- """MCP JSON-RPC endpoint"""
996
- try:
997
- data = await request.json()
998
- method = data.get("method")
999
- params = data.get("params", {})
1000
- request_id = data.get("id")
1001
-
1002
- if method == "initialize":
1003
- response = {
1004
- "jsonrpc": "2.0",
1005
- "id": request_id,
1006
- "result": {
1007
- "protocolVersion": "2024-11-05",
1008
- "capabilities": {
1009
- "tools": {"listChanged": True}
1010
- },
1011
- "serverInfo": {
1012
- "name": "vdi-heat-pump-server",
1013
- "version": "1.0.0"
1014
- }
1015
- }
1016
- }
1017
- elif method == "tools/list":
1018
- response = {
1019
- "jsonrpc": "2.0",
1020
- "id": request_id,
1021
- "result": {
1022
- "tools": MCP_TOOLS
1023
- }
1024
- }
1025
- elif method == "tools/call":
1026
- tool_name = params.get("name")
1027
- arguments = params.get("arguments", {})
1028
- result = route_mcp_tool(tool_name, arguments)
1029
-
1030
- response = {
1031
- "jsonrpc": "2.0",
1032
- "id": request_id,
1033
- "result": {
1034
- "content": [
1035
- {
1036
- "type": "text",
1037
- "text": result
1038
- }
1039
- ]
1040
- }
1041
- }
1042
- else:
1043
- response = {
1044
- "jsonrpc": "2.0",
1045
- "id": request_id,
1046
- "error": {
1047
- "code": -32601,
1048
- "message": f"Method not found: {method}"
1049
- }
1050
  }
 
 
 
1051
 
1052
- return JSONResponse(response)
 
1053
 
1054
- except Exception as e:
1055
- logger.error(f"MCP RPC error: {e}")
1056
- return JSONResponse({
1057
- "jsonrpc": "2.0",
1058
- "id": data.get("id") if 'data' in locals() else None,
1059
- "error": {
1060
- "code": -32603,
1061
- "message": f"Internal error: {str(e)}"
1062
- }
1063
- })
1064
 
1065
- # Launch the app with MCP server support
1066
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
1067
  demo.launch(
 
1068
  share=False,
1069
  server_name="0.0.0.0",
1070
  server_port=7860,
 
220
 
221
  os.makedirs(download_dir, exist_ok=True)
222
 
223
+ try:
224
+ # Get catalog information
225
+ catalog_xml = self.check_catalogs_for_part(part)
226
+ root = ET.fromstring(catalog_xml)
227
+ result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ if result is None:
230
+ raise Exception("No catalog results found")
231
+
232
+ catalogs_xml = ET.fromstring(result.text)
233
+
234
+ # Extract catalog information
235
+ data = []
236
+ for catalog in catalogs_xml.findall('Catalog'):
237
+ data.append({
238
+ 'mfr': catalog.get('mfr'),
239
+ 'nick': catalog.get('nick').split('/')[0].strip(),
240
+ 'part': f"{int(catalog.get('part')):02d}",
241
+ })
242
+
243
+ df = pd.DataFrame(data)
244
+ df['slug'] = df['nick'] + df['part']
245
+ df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip"
246
+
247
+ # Download files
248
+ downloaded_files = []
249
+ for _, row in df.iterrows():
250
+ download_url = row['download_url']
251
+ file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip")
252
 
253
+ try:
254
+ response = requests.get(download_url, timeout=30)
255
+ response.raise_for_status()
256
+
257
+ with open(file_name, 'wb') as file:
258
+ file.write(response.content)
259
+
260
+ downloaded_files.append(file_name)
261
+ logger.info(f"Downloaded: {file_name}")
262
+ except Exception as e:
263
+ logger.error(f"Failed to download {download_url}: {e}")
264
+
265
+ return downloaded_files
266
+
267
+ except Exception as e:
268
+ logger.error(f"Error downloading VDI files: {e}")
269
+ return []
270
 
271
  def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame:
272
  """Parse a single VDI file"""
 
375
  return df
376
 
377
  except Exception as e:
378
+ logger.error(f"Error parsing VDI file {file_path}: {str(e)}")
379
+ return pd.DataFrame()
380
 
381
  def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame:
382
  """Extract ZIP file and parse VDI files"""
 
391
  vdi_files = list(Path(temp_dir).rglob("*.VDI"))
392
 
393
  if not vdi_files:
394
+ logger.warning(f"No VDI files found in {zip_path}")
395
+ return pd.DataFrame()
396
 
397
  all_data = []
398
  for vdi_file in vdi_files:
 
414
  # Initialize the parser
415
  parser = VDIHeatPumpParser()
416
 
417
+ # MCP Tool Functions - These will be automatically exposed as MCP tools
 
418
  def download_vdi_files(part: str, download_dir: str = "") -> str:
419
  """
420
  Download VDI files for a specific part number.
 
422
  Args:
423
  part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
424
  download_dir (str): Directory to save downloaded files (optional)
425
+
426
+ Returns:
427
+ str: JSON response with download results
428
  """
429
  try:
430
  part_int = int(part)
431
  download_dir = download_dir.strip() if download_dir.strip() else None
 
432
  downloaded_files = parser.download_vdi_files(part_int, download_dir)
433
 
434
  result = {
 
439
  "download_directory": download_dir or f"temp directory for part {part_int}"
440
  }
441
  return json.dumps(result, indent=2)
 
442
  except Exception as e:
443
  return f"Error downloading VDI files: {str(e)}"
444
 
 
449
  Args:
450
  part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
451
  download_dir (str): Directory to save downloaded files (optional)
452
+
453
+ Returns:
454
+ str: JSON response with parsing results including heat pump count and manufacturers
455
  """
456
  try:
457
  part_int = int(part)
 
460
  # Download files first
461
  downloaded_files = parser.download_vdi_files(part_int, download_dir)
462
 
463
+ if not downloaded_files:
464
+ return f"No files were downloaded for part {part_int}. Check your internet connection or try a different part number."
465
+
466
  # Parse all downloaded files
467
  all_data = []
468
  for file_path in downloaded_files:
 
483
  "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part_int}",
484
  "part": part_int,
485
  "record_count": len(combined_df),
486
+ "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist()) if 'hersteller' in combined_df.columns else [],
487
  "download_directory": download_dir or f"temp directory for part {part_int}"
488
  }
489
  else:
490
  result = {
491
  "status": "warning",
492
+ "message": f"Files downloaded but no heat pump data found for part {part_int}",
493
  "part": part_int,
494
  "record_count": 0
495
  }
496
 
497
  return json.dumps(result, indent=2)
 
498
  except Exception as e:
499
  return f"Error downloading and parsing part {part}: {str(e)}"
500
 
 
504
 
505
  Args:
506
  file_path (str): Path to the VDI ZIP file
507
+
508
+ Returns:
509
+ str: JSON response with parsing results
510
  """
511
  try:
512
  if not os.path.exists(file_path):
 
521
  "record_count": len(df)
522
  }
523
  return json.dumps(result, indent=2)
 
524
  except Exception as e:
525
  return f"Error parsing file: {str(e)}"
526
 
527
+ def search_heatpump(
528
+ manufacturer: str = "",
529
+ product_name: str = "",
530
+ article_number: str = "",
531
+ heating_power_min: str = "",
532
+ heating_power_max: str = "",
533
+ type: str = ""
534
+ ) -> str:
535
  """
536
  Search for specific heat pump by criteria.
537
 
 
541
  article_number (str): Article number (optional)
542
  heating_power_min (str): Minimum heating power in kW (optional)
543
  heating_power_max (str): Maximum heating power in kW (optional)
544
+ type (str): Heat pump type (e.g., 'Luft-Wasser') (optional)
545
+
546
+ Returns:
547
+ str: JSON array of matching heat pumps with their specifications
548
  """
549
  try:
550
  # Combine all parsed data
 
553
  all_data.append(df)
554
 
555
  if not all_data:
556
+ return "No data available. Please download and parse VDI files first using download_and_parse_part."
557
 
558
  combined_df = pd.concat(all_data, ignore_index=True)
559
  filtered_df = combined_df.copy()
560
 
561
  # Apply filters
562
  if manufacturer.strip():
563
+ if 'hersteller' in filtered_df.columns:
564
+ filtered_df = filtered_df[
565
+ filtered_df['hersteller'].str.contains(manufacturer.strip(), case=False, na=False)
566
+ ]
567
 
568
  if product_name.strip():
569
+ if 'produktname' in filtered_df.columns:
570
+ filtered_df = filtered_df[
571
+ filtered_df['produktname'].str.contains(product_name.strip(), case=False, na=False)
572
+ ]
573
 
574
  if article_number.strip():
575
+ if 'artikelnummer' in filtered_df.columns:
576
+ filtered_df = filtered_df[
577
+ filtered_df['artikelnummer'].str.contains(article_number.strip(), case=False, na=False)
578
+ ]
579
 
580
+ if type.strip():
581
+ if 'typ' in filtered_df.columns:
582
+ filtered_df = filtered_df[
583
+ filtered_df['typ'].str.contains(type.strip(), case=False, na=False)
584
+ ]
585
 
586
  # Filter by heating power
587
  if heating_power_min.strip() or heating_power_max.strip():
588
+ if 'heizleistung' in filtered_df.columns:
589
+ filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce')
590
+
591
+ if heating_power_min.strip():
592
+ min_power = float(heating_power_min.strip())
593
+ filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= min_power]
594
+
595
+ if heating_power_max.strip():
596
+ max_power = float(heating_power_max.strip())
597
+ filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= max_power]
598
 
599
  # Return results
600
  result_columns = ['hersteller', 'produktname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse']
601
  available_columns = [col for col in result_columns if col in filtered_df.columns]
602
+
603
+ if not available_columns:
604
+ return "No matching columns found in the data."
605
+
606
  result_df = filtered_df[available_columns].head(20) # Limit to 20 results
607
 
608
+ if result_df.empty:
609
+ return "No heat pumps found matching your criteria."
610
 
611
+ return result_df.to_json(orient='records', indent=2)
612
  except Exception as e:
613
  return f"Error searching heat pumps: {str(e)}"
614
 
 
618
 
619
  Args:
620
  article_number (str): Article number of the heat pump
621
+
622
+ Returns:
623
+ str: JSON object with all available heat pump specifications and technical details
624
  """
625
  try:
626
  # Search across all parsed data
627
  for df in parser.parsed_files.values():
628
+ if 'artikelnummer' in df.columns:
629
+ matching_rows = df[df['artikelnummer'] == article_number.strip()]
630
+ if not matching_rows.empty:
631
+ details = matching_rows.iloc[0].to_dict()
632
+ # Clean up None values
633
+ details = {k: v for k, v in details.items() if v is not None and v != ''}
634
+ return json.dumps(details, indent=2, default=str)
635
 
636
  return f"Heat pump with article number '{article_number}' not found"
 
637
  except Exception as e:
638
  return f"Error getting heat pump details: {str(e)}"
639
 
640
  def list_manufacturers() -> str:
641
  """
642
  List all available manufacturers in parsed data.
643
+
644
+ Returns:
645
+ str: JSON object with list of manufacturers and count
646
  """
647
  try:
648
  all_manufacturers = set()
 
652
  all_manufacturers.update(manufacturers)
653
 
654
  if not all_manufacturers:
655
+ return "No manufacturers available in the parsed data. Please parse VDI files first using download_and_parse_part."
656
 
657
  result = {
658
  "manufacturers": sorted(list(all_manufacturers)),
659
  "count": len(all_manufacturers)
660
  }
661
  return json.dumps(result, indent=2)
 
662
  except Exception as e:
663
  return f"Error listing manufacturers: {str(e)}"
664
 
665
  def check_storage() -> str:
666
  """
667
  Check available storage locations and permissions.
668
+
669
+ Returns:
670
+ str: JSON object with storage location information and recommendations
671
  """
672
  try:
673
  import tempfile
 
698
  }
699
 
700
  return json.dumps(result, indent=2)
 
701
  except Exception as e:
702
  return f"Error checking storage: {str(e)}"
703
 
704
+ # Create the Gradio interface with MCP tool functions
705
+ with gr.Blocks(title="VDI Heat Pump Parser - MCP Server", theme=gr.themes.Default()) as demo:
706
+ gr.Markdown("# πŸ”₯ VDI Heat Pump Data Parser - MCP Server")
707
+ gr.Markdown("This application provides heat pump data tools for both web interface and Claude Desktop via MCP protocol.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
 
709
  with gr.Tabs():
710
  # Tab 1: Download and Parse
711
+ with gr.TabItem("πŸ“₯ Download & Parse"):
712
  gr.Markdown("## Download and Parse VDI Files")
713
+ gr.Markdown("Download real heat pump data from VDI catalogs and parse it for analysis.")
714
 
715
  with gr.Row():
716
  with gr.Column():
717
  part_input = gr.Textbox(
718
  label="Part Number",
719
  placeholder="Enter part number (e.g., 22 for heat pumps)",
720
+ value="22",
721
+ info="Part 22 = Heat Pumps, Part 2 = Heat Generators"
722
  )
723
  download_dir_input = gr.Textbox(
724
  label="Download Directory (optional)",
725
+ placeholder="Leave empty for automatic temp directory",
726
+ info="Files will be saved to this directory"
727
  )
728
 
729
  with gr.Column():
730
+ download_btn = gr.Button("πŸ“ Download Files Only", variant="secondary")
731
+ parse_btn = gr.Button("πŸš€ Download & Parse", variant="primary")
732
 
733
  download_output = gr.Textbox(
734
  label="Results",
735
+ lines=12,
736
+ interactive=False,
737
+ placeholder="Results will appear here..."
738
  )
739
 
740
  download_btn.click(
 
750
  )
751
 
752
  # Tab 2: Search Heat Pumps
753
+ with gr.TabItem("πŸ” Search Heat Pumps"):
754
  gr.Markdown("## Search for Heat Pumps")
755
+ gr.Markdown("Search through parsed heat pump data using various criteria.")
756
 
757
  with gr.Row():
758
  with gr.Column():
759
+ manufacturer_input = gr.Textbox(
760
+ label="Manufacturer",
761
+ placeholder="e.g., Viessmann, Vaillant, Bosch",
762
+ info="Search by manufacturer name"
763
+ )
764
+ product_name_input = gr.Textbox(
765
+ label="Product Name",
766
+ placeholder="e.g., Vitocal, EcoTEC",
767
+ info="Search by product name or series"
768
+ )
769
+ article_number_input = gr.Textbox(
770
+ label="Article Number",
771
+ placeholder="Exact or partial article number",
772
+ info="Search by specific article number"
773
+ )
774
 
775
  with gr.Column():
776
+ type_input = gr.Textbox(
777
+ label="Heat Pump Type",
778
+ placeholder="e.g., Luft-Wasser, Wasser-Wasser",
779
+ info="Search by heat pump technology type"
780
+ )
781
+ min_power_input = gr.Textbox(
782
+ label="Min Heating Power (kW)",
783
+ placeholder="e.g., 5",
784
+ info="Minimum heating power in kilowatts"
785
+ )
786
+ max_power_input = gr.Textbox(
787
+ label="Max Heating Power (kW)",
788
+ placeholder="e.g., 20",
789
+ info="Maximum heating power in kilowatts"
790
+ )
791
 
792
+ search_btn = gr.Button("πŸ” Search Heat Pumps", variant="primary")
793
 
794
  search_output = gr.Textbox(
795
  label="Search Results",
796
+ lines=18,
797
+ interactive=False,
798
+ placeholder="Search results will appear here..."
799
  )
800
 
801
  search_btn.click(
 
806
  )
807
 
808
  # Tab 3: Heat Pump Details
809
+ with gr.TabItem("πŸ“‹ Heat Pump Details"):
810
  gr.Markdown("## Get Detailed Information")
811
+ gr.Markdown("Enter an article number to get complete technical specifications.")
812
 
813
  article_detail_input = gr.Textbox(
814
  label="Article Number",
815
+ placeholder="Enter exact article number",
816
+ info="Copy an article number from search results"
817
  )
818
 
819
+ detail_btn = gr.Button("πŸ“‹ Get Details", variant="primary")
820
 
821
  detail_output = gr.Textbox(
822
  label="Heat Pump Details",
823
+ lines=25,
824
+ interactive=False,
825
+ placeholder="Detailed specifications will appear here..."
826
  )
827
 
828
  detail_btn.click(
 
832
  )
833
 
834
  # Tab 4: Utilities
835
+ with gr.TabItem("πŸ› οΈ Utilities"):
836
  gr.Markdown("## Utility Functions")
837
+ gr.Markdown("Check system status and list available manufacturers.")
838
 
839
  with gr.Row():
840
+ with gr.Column():
841
+ manufacturers_btn = gr.Button("πŸ‘₯ List Manufacturers", variant="secondary")
842
+ gr.Markdown("*Shows all manufacturers in the parsed data*")
843
+
844
+ with gr.Column():
845
+ storage_btn = gr.Button("πŸ’Ύ Check Storage", variant="secondary")
846
+ gr.Markdown("*Checks file system permissions and storage locations*")
847
 
848
  utility_output = gr.Textbox(
849
  label="Results",
850
+ lines=12,
851
+ interactive=False,
852
+ placeholder="Utility results will appear here..."
853
  )
854
 
855
  manufacturers_btn.click(
 
863
  )
864
 
865
  # Tab 5: Parse Local File
866
+ with gr.TabItem("πŸ“ Parse Local File"):
867
  gr.Markdown("## Parse Local VDI ZIP File")
868
+ gr.Markdown("Parse VDI files that you have downloaded manually.")
869
 
870
  file_path_input = gr.Textbox(
871
  label="File Path",
872
+ placeholder="Enter full path to VDI ZIP file",
873
+ info="e.g., /path/to/PART22_manufacturer.zip"
874
  )
875
 
876
+ parse_file_btn = gr.Button("πŸ“ Parse File", variant="primary")
877
 
878
  parse_file_output = gr.Textbox(
879
  label="Parse Results",
880
+ lines=12,
881
+ interactive=False,
882
+ placeholder="Parse results will appear here..."
883
  )
884
 
885
  parse_file_btn.click(
 
887
  inputs=file_path_input,
888
  outputs=parse_file_output
889
  )
 
 
 
 
 
 
 
 
 
 
 
 
890
 
891
+ # Tab 6: MCP Info
892
+ with gr.TabItem("πŸ”Œ MCP Info"):
893
+ gr.Markdown("## MCP Server Information")
894
+ gr.Markdown("This application exposes the following tools for Claude Desktop:")
895
+
896
+ gr.Markdown("""
897
+ ### Available MCP Tools:
898
+
899
+ - **`download_vdi_files`** - Download VDI files for a specific part number
900
+ - **`download_and_parse_part`** - Download and parse VDI files automatically
901
+ - **`parse_vdi_file`** - Parse a local VDI ZIP file
902
+ - **`search_heatpump`** - Search heat pumps by manufacturer, power, type, etc.
903
+ - **`get_heatpump_details`** - Get detailed specifications for a specific heat pump
904
+ - **`list_manufacturers`** - List all available manufacturers
905
+ - **`check_storage`** - Check storage locations and permissions
906
+
907
+ ### Claude Desktop Configuration:
908
+
909
+ ```json
910
+ {
911
+ "mcpServers": {
912
+ "heat-pump": {
913
+ "command": "npx",
914
+ "args": [
915
+ "mcp-remote",
916
+ "YOUR_SERVER_URL/gradio_api/mcp/sse"
917
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  }
919
+ }
920
+ }
921
+ ```
922
 
923
+ ### Local Testing:
924
+ Replace `YOUR_SERVER_URL` with `http://localhost:7860` for local testing.
925
 
926
+ ### Production (Hugging Face):
927
+ Replace `YOUR_SERVER_URL` with your Hugging Face Space URL.
928
+ """)
 
 
 
 
 
 
 
929
 
930
+ # Launch the application with MCP server enabled
931
  if __name__ == "__main__":
932
+ print("=" * 60)
933
+ print("πŸ”₯ VDI Heat Pump Parser - MCP Server")
934
+ print("=" * 60)
935
+ print("🌐 Starting Gradio interface with MCP server support...")
936
+ print("πŸ”Œ MCP tools will be automatically exposed for Claude Desktop")
937
+ print("=" * 60)
938
+
939
+ # Launch with MCP server enabled - this is the key line!
940
  demo.launch(
941
+ mcp_server=True, # This enables MCP server functionality
942
  share=False,
943
  server_name="0.0.0.0",
944
  server_port=7860,
requirements.txt CHANGED
@@ -1,7 +1,4 @@
1
- gradio>=4.0.0
2
  pandas>=2.0.0
3
  numpy>=1.24.0
4
  requests>=2.28.0
5
- fastapi>=0.68.0
6
- uvicorn>=0.15.0
7
- pathlib2>=2.3.7
 
1
+ gradio[mcp]>=4.0.0
2
  pandas>=2.0.0
3
  numpy>=1.24.0
4
  requests>=2.28.0