| | |
| | """ |
| | Gradio MCP Server for VDI Heat Pump Data Parsing and Querying |
| | Provides tools to download, parse VDI files and query specific heat pump information |
| | """ |
| |
|
| | import asyncio |
| | import json |
| | import os |
| | import sys |
| | import pandas as pd |
| | import numpy as np |
| | from typing import Any, Dict, List, Optional |
| | import zipfile |
| | import tempfile |
| | from pathlib import Path |
| | import requests |
| | import xml.etree.ElementTree as ET |
| | import gradio as gr |
| |
|
| | |
| | import logging |
| | logging.basicConfig(level=logging.DEBUG) |
| |
|
| | |
| | def parse_010(r): |
| | """Parse 010:Vorlaufdaten""" |
| | return { |
| | 'blatt': r[1] if len(r) > 1 else None, |
| | 'hersteller': r[3] if len(r) > 3 else None, |
| | 'datum': r[4] if len(r) > 4 else None, |
| | } |
| |
|
| | def parse_100(r): |
| | """Parse 100:Einsatzbereich""" |
| | return { |
| | 'index_100': int(r[1]) if len(r) > 1 else None, |
| | 'einsatzbereich': r[3] if len(r) > 3 else None, |
| | } |
| |
|
| | def parse_110(r): |
| | """Parse 110:Typ""" |
| | return { |
| | 'index_110': int(r[1]) if len(r) > 1 else None, |
| | 'typ': r[3] if len(r) > 3 else None, |
| | } |
| |
|
| | def parse_400(r): |
| | """Parse 400:Bauart""" |
| | return { |
| | 'index_400': int(r[1]) if len(r) > 1 else None, |
| | 'bauart': r[2] if len(r) > 2 else None, |
| | } |
| |
|
| | def parse_450(r): |
| | """Parse 450:Aufstellungsort""" |
| | return { |
| | 'index_450': int(r[1]) if len(r) > 1 else None, |
| | 'aufstellungsort': r[2] if len(r) > 2 else None, |
| | } |
| |
|
| | def parse_460(r): |
| | """Parse 460:Leistungsregelung der Wärmepumpe""" |
| | return { |
| | 'index_460': int(r[1]) if len(r) > 1 else None, |
| | 'leistungsregelung_der_wärmepumpe': r[2] if len(r) > 2 else None, |
| | } |
| |
|
| | def parse_700(r): |
| | """Parse 700:Produktelementdaten""" |
| | return { |
| | 'index_700': int(r[1]) if len(r) > 1 else None, |
| | 'sortiernummer': r[2] if len(r) > 2 else None, |
| | 'produktname': r[3] if len(r) > 3 else None, |
| | 'heizleistung': r[4] if len(r) > 4 else None, |
| | 'leistungszahl': r[5] if len(r) > 5 else None, |
| | 'elektrische_aufnahmeleistung_wärmepumpe': r[6] if len(r) > 6 else None, |
| | 'leistung_der_elektrischen_zusatzheizung': r[7] if len(r) > 7 else None, |
| | 'elektroanschluss': r[8] if len(r) > 8 else None, |
| | 'anlaufstrom': r[9] if len(r) > 9 else None, |
| | 'index_auf_satzart_200_wärmequelle': r[10] if len(r) > 10 else None, |
| | 'eingesetztes_kältemittel': r[11] if len(r) > 11 else None, |
| | 'füllmenge_des_kältemittels': r[12] if len(r) > 12 else None, |
| | 'schallleistungspegel': r[13] if len(r) > 13 else None, |
| | 'schutzart_nach_din_en_60529': r[14] if len(r) > 14 else None, |
| | 'maximale_vorlauftemperatur': r[15] if len(r) > 15 else None, |
| | 'heizwassertemperaturspreizung': r[16] if len(r) > 16 else None, |
| | 'trinkwasser_erwärmung_über_indirekt_beheizten_speicher': r[17] if len(r) > 17 else None, |
| | 'kühlfunktion': r[19] if len(r) > 19 else None, |
| | 'kühlleistung': r[20] if len(r) > 20 else None, |
| | 'verdichteranzahl': r[21] if len(r) > 21 else None, |
| | 'leistungsstufen': r[22] if len(r) > 22 else None, |
| | 'produktserie': r[32] if len(r) > 32 else None, |
| | 'treibhauspotential_gwp': r[33] if len(r) > 33 else None, |
| | 'bauart_des_kältekreis': r[35] if len(r) > 35 else None, |
| | 'sicherheitsklasse_nach_din_en_378_1': r[36] if len(r) > 36 else None, |
| | 'hinweistext_zum_kältemittel': r[37] if len(r) > 37 else None, |
| | 'sanftanlasser': r[38] if len(r) > 38 else None, |
| | } |
| |
|
| | def parse_710_01(r): |
| | """Parse 710.01:Heizungs-Wärmepumpe""" |
| | return { |
| | 'index_710_01': int(r[1]) if len(r) > 1 else None, |
| | 'korrekturfaktor_7_k': r[2] if len(r) > 2 else None, |
| | 'korrekturfaktor_10_k': r[3] if len(r) > 3 else None, |
| | 'temperaturdifferenz_am_verflüssiger_bei_prüfstandmessung': r[4] if len(r) > 4 else None, |
| | } |
| |
|
| | def parse_710_04(r): |
| | """Parse 710.04:Wasser-Wasser-Wärmepumpe""" |
| | return { |
| | 'index_710_04': int(r[1]) if len(r) > 1 else None, |
| | 'leistungszahl_bei_w10_w35': r[2] if len(r) > 2 else None, |
| | 'elektrische_leistungsaufnahme_wärmequellenpumpe': r[3] if len(r) > 3 else None, |
| | 'heizleistung': r[4] if len(r) > 4 else None, |
| | 'einsatzgrenze_wärmequelle_von': r[5] if len(r) > 5 else None, |
| | 'einsatzgrenze_wärmequelle_bis': r[6] if len(r) > 6 else None, |
| | 'volumenstrom_heizungsseitig': r[7] if len(r) > 7 else None, |
| | 'wärmequellenpumpe_intern': r[8] if len(r) > 8 else None, |
| | 'heizkreispumpe_intern': r[9] if len(r) > 9 else None, |
| | 'elektrische_leistungsaufnahme_heizkreispumpe': r[10] if len(r) > 10 else None, |
| | 'leistungszahl_bei_w10_w45': r[11] if len(r) > 11 else None, |
| | 'leistungszahl_bei_w10_w55': r[12] if len(r) > 12 else None, |
| | 'leistungszahl_bei_w7_w35': r[13] if len(r) > 13 else None, |
| | 'kühlleistung_bei_w15_w23': r[14] if len(r) > 14 else None, |
| | 'kühlleistungszahl_bei_w15_w23': r[15] if len(r) > 15 else None, |
| | 'minimaler_volumenstrom_wärmequelle': r[16] if len(r) > 16 else None, |
| | 'maximaler_volumenstrom_wärmequelle': r[17] if len(r) > 17 else None, |
| | } |
| |
|
| | def parse_710_05(r): |
| | """Parse 710.05:Luft-Wasser-Wärmepumpe""" |
| | return { |
| | 'index_710_05': int(r[1]) if len(r) > 1 else None, |
| | 'leistungszahl_bei_a_7_w35': r[2] if len(r) > 2 else None, |
| | 'leistungszahl_bei_a2_w35': r[3] if len(r) > 3 else None, |
| | 'leistungszahl_bei_a10_w35': r[4] if len(r) > 4 else None, |
| | 'abtauart': r[5] if len(r) > 5 else None, |
| | 'kühlfunktion_durch_kreislaufumkehr': r[6] if len(r) > 6 else None, |
| | 'leistungszahl_bei_a7_w35': r[7] if len(r) > 7 else None, |
| | 'leistungszahl_bei_a_15_w35': r[8] if len(r) > 8 else None, |
| | 'leistungszahl_bei_a2_w45': r[9] if len(r) > 9 else None, |
| | 'leistungszahl_bei_a7_w45': r[10] if len(r) > 10 else None, |
| | 'leistungszahl_bei_a_7_w55': r[11] if len(r) > 11 else None, |
| | 'leistungszahl_bei_a7_w55': r[12] if len(r) > 12 else None, |
| | 'leistungszahl_bei_a10_w55': r[13] if len(r) > 13 else None, |
| | 'kühlleistungszahl_bei_a35_w7': r[14] if len(r) > 14 else None, |
| | 'kühlleistungszahl_bei_a35_w18': r[15] if len(r) > 15 else None, |
| | 'kühlleistung_bei_a35_w7': r[16] if len(r) > 16 else None, |
| | 'kühlleistung_bei_a35_w18': r[17] if len(r) > 17 else None, |
| | 'minimale_einsatzgrenze_wärmequelle': r[18] if len(r) > 18 else None, |
| | 'maximale_einsatzgrenze_wärmequelle': r[19] if len(r) > 19 else None, |
| | 'leistungszahl_a20_w35': r[20] if len(r) > 20 else None, |
| | 'leistungszahl_a20_w45': r[21] if len(r) > 21 else None, |
| | 'leistungszahl_a20_w55': r[22] if len(r) > 22 else None, |
| | 'leistungsaufnahme_luefter': r[25] if len(r) > 25 else None, |
| | 'volumenstrom_heizungsseitig': r[26] if len(r) > 26 else None, |
| | } |
| |
|
| | def parse_710_07(r): |
| | """Parse 710.07:Einbringmaße""" |
| | return { |
| | 'index_710_07': int(r[1]) if len(r) > 1 else None, |
| | 'art_der_maße': r[2] if len(r) > 2 else None, |
| | 'länge': r[3] if len(r) > 3 else None, |
| | 'breite': r[4] if len(r) > 4 else None, |
| | 'höhe': r[5] if len(r) > 5 else None, |
| | 'masse': r[6] if len(r) > 6 else None, |
| | 'beschreibung': r[7] if len(r) > 7 else None, |
| | } |
| |
|
| | def parse_800(r): |
| | """Parse 800:TGA-Nummer""" |
| | return { |
| | 'index_800': int(r[1]) if len(r) > 1 else None, |
| | 'tga_nummer': r[2] if len(r) > 2 else None, |
| | } |
| |
|
| | def parse_810(r): |
| | """Parse 810:Artikelnummern""" |
| | return { |
| | 'index_810': int(r[1]) if len(r) > 1 else None, |
| | 'artikelnummer': r[2] if len(r) > 2 else None, |
| | 'artikelname': r[9] if len(r) > 9 else None, |
| | 'energieeffizienzklasse': r[10] if len(r) > 10 else None, |
| | 'erp_richtlinie': r[11] if len(r) > 11 else None, |
| | } |
| |
|
| | class VDIHeatPumpParser: |
| | """Main parser class for VDI heat pump data""" |
| | |
| | def __init__(self): |
| | self.data_cache = {} |
| | self.parsed_files = {} |
| | self.domain = "bim4hvac.com" |
| | |
| | def check_catalogs_for_part(self, part: int) -> str: |
| | """Check if catalogs have been updated for a part""" |
| | try: |
| | url = f"http://catalogue.{self.domain}/bdh/ws/mcc.asmx" |
| | payload = f"""<?xml version="1.0" encoding="utf-8"?> |
| | <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> |
| | <soap:Body> |
| | <CheckCatalogsForPart xmlns="urn:bdhmcc"> |
| | <part>{part}</part> |
| | </CheckCatalogsForPart> |
| | </soap:Body> |
| | </soap:Envelope> |
| | """ |
| | headers = {'Content-Type': 'text/xml; charset=utf-8'} |
| | response = requests.post(url, headers=headers, data=payload, timeout=30) |
| | return response.text |
| | except Exception as e: |
| | raise Exception(f"Error checking catalogs for part {part}: {str(e)}") |
| | |
| | def download_vdi_files(self, part: int, download_dir: str = None) -> List[str]: |
| | """Download all VDI files for a given part""" |
| | try: |
| | if download_dir is None: |
| | download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}") |
| | |
| | os.makedirs(download_dir, exist_ok=True) |
| | |
| | |
| | catalog_xml = self.check_catalogs_for_part(part) |
| | |
| | |
| | root = ET.fromstring(catalog_xml) |
| | result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult') |
| | |
| | if result is None: |
| | raise Exception("No catalog results found") |
| | |
| | |
| | catalogs_xml = ET.fromstring(result.text) |
| | |
| | |
| | data = [] |
| | for catalog in catalogs_xml.findall('Catalog'): |
| | data.append({ |
| | 'mfr': catalog.get('mfr'), |
| | 'nick': catalog.get('nick').split('/')[0].strip(), |
| | 'part': f"{int(catalog.get('part')):02d}", |
| | }) |
| |
|
| | df = pd.DataFrame(data) |
| | |
| | |
| | df['slug'] = df['nick'] + df['part'] |
| | df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip" |
| | |
| | |
| | downloaded_files = [] |
| | for _, row in df.iterrows(): |
| | download_url = row['download_url'] |
| | file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip") |
| | |
| | print(f"Downloading {download_url}...") |
| | response = requests.get(download_url, timeout=30) |
| | response.raise_for_status() |
| | |
| | with open(file_name, 'wb') as file: |
| | file.write(response.content) |
| | |
| | downloaded_files.append(file_name) |
| | print(f"Downloaded: {file_name}") |
| | |
| | return downloaded_files |
| | |
| | except Exception as e: |
| | raise Exception(f"Error downloading VDI files for part {part}: {str(e)}") |
| | |
| | def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame: |
| | """Parse a single VDI file using the full parsing logic""" |
| | try: |
| | with open(file_path, 'r', encoding="latin-1") as file: |
| | text = file.read() |
| | |
| | |
| | lines = np.array(text.split("\n")) |
| | records = list(map(lambda x: x.split(";"), lines)) |
| | |
| | |
| | r010 = parse_010([]) |
| | r100 = parse_100([]) |
| | r110 = parse_110([]) |
| | r400 = parse_400([]) |
| | r450 = parse_450([]) |
| | r460 = parse_460([]) |
| | r700 = parse_700([]) |
| | r710_01 = parse_710_01([]) |
| | r710_04 = parse_710_04([]) |
| | r710_05 = parse_710_05([]) |
| | r710_07 = parse_710_07([]) |
| | r800 = parse_800([]) |
| | r810 = parse_810([]) |
| |
|
| | |
| | data = [] |
| | for r in records: |
| | if r[0] == '010': |
| | r010 = parse_010(r) |
| | if r[0] == '100': |
| | r100 = parse_100(r) |
| | elif r[0] == '110': |
| | r110 = parse_110(r) |
| | |
| | r400s = [] |
| | r450s = [] |
| | r460s = [] |
| | r700s = [] |
| | r710_07s = {} |
| | elif r[0] == '400': |
| | r400 = parse_400(r) |
| | r400s.append(r400) |
| | elif r[0] == '450': |
| | r450 = parse_450(r) |
| | r450s.append(r450) |
| | elif r[0] == '460': |
| | r460 = parse_460(r) |
| | r460s.append(r460) |
| | elif r[0] == '700': |
| | r700 = parse_700(r) |
| | r700s.append(r700) |
| | elif r[0] == '710.01': |
| | r710_01 = parse_710_01(r) |
| | if r700s: |
| | r700s[-1].update(r710_01) |
| | elif r[0] == '710.04': |
| | r710_04 = parse_710_04(r) |
| | if r700s: |
| | r700s[-1].update(r710_04) |
| | elif r[0] == '710.05': |
| | r710_05 = parse_710_05(r) |
| | if r700s: |
| | r700s[-1].update(r710_05) |
| | elif r[0] == '710.07': |
| | r710_07 = parse_710_07(r) |
| | if r700s: |
| | r710_07s.setdefault(r700s[-1]['index_700'], []).append(r710_07) |
| | elif r[0] == '800': |
| | r800 = parse_800(r) |
| | |
| | |
| | if r800['tga_nummer'] and len(r800['tga_nummer']) >= 50: |
| | try: |
| | index_400 = int(r800['tga_nummer'][27:30]) |
| | index_450 = int(r800['tga_nummer'][30:33]) |
| | index_460 = int(r800['tga_nummer'][33:36]) |
| | index_700 = int(r800['tga_nummer'][45:50]) |
| | |
| | |
| | r400 = next((r for r in r400s if r['index_400'] == index_400), {}) |
| | r450 = next((r for r in r450s if r['index_450'] == index_450), {}) |
| | r460 = next((r for r in r460s if r['index_460'] == index_460), {}) |
| | r700 = next((r for r in r700s if r['index_700'] == index_700), {}) |
| | |
| | |
| | filtered_r710_07s = [r for r in r710_07s.get(index_700, []) if r['art_der_maße'] == '2'] |
| | if len(filtered_r710_07s) == 1: |
| | r710_07 = filtered_r710_07s[0] |
| | else: |
| | aufstellungsort = r450.get('aufstellungsort', '').lower() |
| | r710_07 = next((r for r in filtered_r710_07s if r.get('beschreibung', '').lower().startswith(aufstellungsort)), {}) |
| | except (ValueError, IndexError): |
| | |
| | r400, r450, r460, r700, r710_07 = {}, {}, {}, {}, {} |
| | |
| | elif r[0] == '810': |
| | r810 = parse_810(r) |
| | if r700: |
| | data.append({**r810, **r800, **r710_07, **r700, **r460, **r450, **r400, **r110, **r100, **r010}) |
| |
|
| | |
| | df = pd.DataFrame(data) |
| | |
| | |
| | df['nick'] = nick |
| | |
| | |
| | df = df.replace({r'[\r\n]+': ' '}, regex=True) |
| | |
| | |
| | df = df.replace({r'¶': ', '}, regex=True) |
| | |
| | |
| | index_columns = [col for col in df.columns if col.startswith('index_')] |
| | for col in index_columns: |
| | if col in df.columns: |
| | df[col] = df[col].replace('', pd.NA).astype('Int64') |
| | |
| | return df |
| | |
| | except Exception as e: |
| | raise Exception(f"Error parsing VDI file {file_path}: {str(e)}") |
| | |
| | def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame: |
| | """Extract ZIP file and parse VDI files""" |
| | nick = Path(zip_path).stem.split('_')[-1] |
| | |
| | with tempfile.TemporaryDirectory() as temp_dir: |
| | with zipfile.ZipFile(zip_path, 'r') as zip_ref: |
| | zip_ref.extractall(temp_dir) |
| | |
| | |
| | vdi_files = list(Path(temp_dir).rglob("*.vdi")) |
| | if not vdi_files: |
| | vdi_files = list(Path(temp_dir).rglob("*.VDI")) |
| | |
| | if not vdi_files: |
| | raise Exception(f"No VDI files found in {zip_path}") |
| | |
| | all_data = [] |
| | for vdi_file in vdi_files: |
| | try: |
| | df = self.parse_vdi_file(str(vdi_file), nick) |
| | if not df.empty: |
| | all_data.append(df) |
| | except Exception as e: |
| | print(f"Error parsing {vdi_file.name}: {e}") |
| | continue |
| | |
| | if all_data: |
| | combined_df = pd.concat(all_data, ignore_index=True) |
| | self.parsed_files[zip_path] = combined_df |
| | return combined_df |
| | |
| | return pd.DataFrame() |
| |
|
| | |
| | parser = VDIHeatPumpParser() |
| |
|
| | |
| | def download_and_parse_part(part: int) -> str: |
| | """Download and parse VDI files for a specific part""" |
| | try: |
| | download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}") |
| | |
| | |
| | downloaded_files = parser.download_vdi_files(part, download_dir) |
| | |
| | |
| | all_data = [] |
| | for file_path in downloaded_files: |
| | try: |
| | df = parser.extract_and_parse_zip(file_path) |
| | if not df.empty: |
| | all_data.append(df) |
| | except Exception as e: |
| | print(f"Error parsing {file_path}: {e}") |
| | |
| | if all_data: |
| | combined_df = pd.concat(all_data, ignore_index=True) |
| | cache_key = f"part_{part}" |
| | parser.parsed_files[cache_key] = combined_df |
| | |
| | result = { |
| | "status": "success", |
| | "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part}", |
| | "part": part, |
| | "record_count": len(combined_df), |
| | "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist()) |
| | } |
| | else: |
| | result = { |
| | "status": "warning", |
| | "message": f"No heat pump data found for part {part}", |
| | "part": part, |
| | "record_count": 0 |
| | } |
| | |
| | return json.dumps(result, indent=2) |
| | |
| | except Exception as e: |
| | return json.dumps({"status": "error", "message": f"Error: {str(e)}"}, indent=2) |
| |
|
| | def search_heatpump(manufacturer: str = None, product_name: str = None, article_number: str = None, |
| | heating_power_min: float = None, heating_power_max: float = None, heat_pump_type: str = None) -> str: |
| | """Search for heat pumps based on criteria""" |
| | try: |
| | |
| | all_data = [] |
| | for df in parser.parsed_files.values(): |
| | all_data.append(df) |
| | |
| | if not all_data: |
| | return json.dumps({"status": "error", "message": "No data available. Please parse VDI files first."}) |
| | |
| | combined_df = pd.concat(all_data, ignore_index=True) |
| | |
| | |
| | filtered_df = combined_df.copy() |
| | |
| | if manufacturer: |
| | filtered_df = filtered_df[ |
| | filtered_df['hersteller'].str.contains(manufacturer, case=False, na=False) |
| | ] |
| | |
| | if product_name: |
| | filtered_df = filtered_df[ |
| | filtered_df['produktname'].str.contains(product_name, case=False, na=False) |
| | ] |
| | |
| | if article_number: |
| | filtered_df = filtered_df[ |
| | filtered_df['artikelnummer'].str.contains(article_number, case=False, na=False) |
| | ] |
| | |
| | if heat_pump_type: |
| | filtered_df = filtered_df[ |
| | filtered_df['typ'].str.contains(heat_pump_type, case=False, na=False) |
| | ] |
| | |
| | |
| | if heating_power_min is not None or heating_power_max is not None: |
| | filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce') |
| | |
| | if heating_power_min is not None: |
| | filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= heating_power_min] |
| | |
| | if heating_power_max is not None: |
| | filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= heating_power_max] |
| | |
| | |
| | result_columns = ['hersteller', 'produktname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse'] |
| | available_columns = [col for col in result_columns if col in filtered_df.columns] |
| | result_df = filtered_df[available_columns].head(20) |
| | |
| | return result_df.to_json(orient='records', indent=2) |
| | |
| | except Exception as e: |
| | return json.dumps({"status": "error", "message": f"Error searching heat pumps: {str(e)}"}, indent=2) |
| |
|
| | def get_heatpump_details(article_number: str) -> str: |
| | """Get detailed information about a specific heat pump""" |
| | try: |
| | |
| | for df in parser.parsed_files.values(): |
| | matching_rows = df[df['artikelnummer'] == article_number] |
| | if not matching_rows.empty: |
| | details = matching_rows.iloc[0].to_dict() |
| | |
| | details = {k: v for k, v in details.items() if v is not None and v != ''} |
| | return json.dumps(details, indent=2, default=str) |
| | |
| | return json.dumps({"status": "error", "message": f"Heat pump with article number '{article_number}' not found"}) |
| | |
| | except Exception as e: |
| | return json.dumps({"status": "error", "message": f"Error getting heat pump details: {str(e)}"}, indent=2) |
| |
|
| | def list_manufacturers() -> str: |
| | """List all manufacturers in the parsed data""" |
| | try: |
| | all_manufacturers = set() |
| | for df in parser.parsed_files.values(): |
| | if 'hersteller' in df.columns: |
| | manufacturers = df['hersteller'].dropna().unique() |
| | all_manufacturers.update(manufacturers) |
| | |
| | if not all_manufacturers: |
| | return json.dumps({ |
| | "status": "warning", |
| | "message": "No manufacturers available. Please parse VDI files first.", |
| | "manufacturers": [], |
| | "count": 0 |
| | }) |
| | |
| | result = { |
| | "status": "success", |
| | "manufacturers": sorted(list(all_manufacturers)), |
| | "count": len(all_manufacturers) |
| | } |
| | return json.dumps(result, indent=2) |
| | |
| | except Exception as e: |
| | return json.dumps({"status": "error", "message": f"Error listing manufacturers: {str(e)}"}, indent=2) |
| |
|
| | def get_data_summary() -> str: |
| | """Get summary statistics of all parsed data""" |
| | try: |
| | if not parser.parsed_files: |
| | return json.dumps({ |
| | "status": "warning", |
| | "message": "No data available. Please parse VDI files first.", |
| | "total_records": 0 |
| | }) |
| | |
| | total_records = 0 |
| | manufacturers = set() |
| | heat_pump_types = set() |
| | |
| | for df in parser.parsed_files.values(): |
| | total_records += len(df) |
| | if 'hersteller' in df.columns: |
| | manufacturers.update(df['hersteller'].dropna().unique()) |
| | if 'typ' in df.columns: |
| | heat_pump_types.update(df['typ'].dropna().unique()) |
| | |
| | result = { |
| | "status": "success", |
| | "total_records": total_records, |
| | "manufacturer_count": len(manufacturers), |
| | "heat_pump_types": sorted(list(heat_pump_types)), |
| | "parsed_files": len(parser.parsed_files) |
| | } |
| | return json.dumps(result, indent=2) |
| | |
| | except Exception as e: |
| | return json.dumps({"status": "error", "message": f"Error getting data summary: {str(e)}"}, indent=2) |
| |
|
| | |
| | def create_interface(): |
| | """Create Gradio interface with MCP server""" |
| | |
| | def test_download_and_parse(part_number): |
| | """Test the download and parse functionality""" |
| | if not part_number: |
| | return "Please enter a part number" |
| | |
| | try: |
| | part = int(part_number) |
| | result = download_and_parse_part(part) |
| | return result |
| | except ValueError: |
| | return "Please enter a valid integer for part number" |
| | except Exception as e: |
| | return f"Error: {str(e)}" |
| | |
| | def test_search(manufacturer, product_name, article_number, heating_power_min, heating_power_max, heat_pump_type): |
| | """Test the search functionality""" |
| | try: |
| | |
| | manufacturer = manufacturer if manufacturer.strip() else None |
| | product_name = product_name if product_name.strip() else None |
| | article_number = article_number if article_number.strip() else None |
| | heat_pump_type = heat_pump_type if heat_pump_type.strip() else None |
| | |
| | |
| | heating_power_min = float(heating_power_min) if heating_power_min else None |
| | heating_power_max = float(heating_power_max) if heating_power_max else None |
| | |
| | result = search_heatpump( |
| | manufacturer=manufacturer, |
| | product_name=product_name, |
| | article_number=article_number, |
| | heating_power_min=heating_power_min, |
| | heating_power_max=heating_power_max, |
| | heat_pump_type=heat_pump_type |
| | ) |
| | return result |
| | except Exception as e: |
| | return f"Error: {str(e)}" |
| | |
| | def test_get_details(article_number): |
| | """Test getting heat pump details""" |
| | if not article_number.strip(): |
| | return "Please enter an article number" |
| | |
| | result = get_heatpump_details(article_number.strip()) |
| | return result |
| | |
| | def test_list_manufacturers(): |
| | """Test listing manufacturers""" |
| | result = list_manufacturers() |
| | return result |
| | |
| | def test_get_summary(): |
| | """Test getting data summary""" |
| | result = get_data_summary() |
| | return result |
| | |
| | |
| | with gr.Blocks(title="VDI Heat Pump Parser - MCP Server") as demo: |
| | gr.Markdown("# VDI Heat Pump Parser - MCP Server") |
| | gr.Markdown("This interface allows you to test the MCP tools for parsing VDI heat pump data.") |
| | |
| | with gr.Tab("Download & Parse"): |
| | with gr.Row(): |
| | part_input = gr.Textbox(label="Part Number", placeholder="Enter part number (e.g., 22 for heat pumps)") |
| | download_btn = gr.Button("Download & Parse") |
| | download_output = gr.Textbox(label="Result", lines=10) |
| | download_btn.click(test_download_and_parse, inputs=[part_input], outputs=[download_output]) |
| | |
| | with gr.Tab("Search Heat Pumps"): |
| | with gr.Row(): |
| | with gr.Column(): |
| | search_manufacturer = gr.Textbox(label="Manufacturer", placeholder="e.g., Viessmann") |
| | search_product = gr.Textbox(label="Product Name", placeholder="e.g., Vitocal") |
| | search_article = gr.Textbox(label="Article Number", placeholder="e.g., 12345") |
| | with gr.Column(): |
| | search_power_min = gr.Textbox(label="Min Heating Power (kW)", placeholder="e.g., 5") |
| | search_power_max = gr.Textbox(label="Max Heating Power (kW)", placeholder="e.g., 15") |
| | search_type = gr.Textbox(label="Heat Pump Type", placeholder="e.g., Luft-Wasser") |
| | |
| | search_btn = gr.Button("Search") |
| | search_output = gr.Textbox(label="Search Results", lines=15) |
| | |
| | search_btn.click( |
| | test_search, |
| | inputs=[search_manufacturer, search_product, search_article, search_power_min, search_power_max, search_type], |
| | outputs=[search_output] |
| | ) |
| | |
| | with gr.Tab("Get Details"): |
| | with gr.Row(): |
| | details_article = gr.Textbox(label="Article Number", placeholder="Enter exact article number") |
| | details_btn = gr.Button("Get Details") |
| | details_output = gr.Textbox(label="Heat Pump Details", lines=20) |
| | details_btn.click(test_get_details, inputs=[details_article], outputs=[details_output]) |
| | |
| | with gr.Tab("Data Management"): |
| | with gr.Row(): |
| | with gr.Column(): |
| | manufacturers_btn = gr.Button("List Manufacturers") |
| | manufacturers_output = gr.Textbox(label="Manufacturers", lines=10) |
| | manufacturers_btn.click(test_list_manufacturers, outputs=[manufacturers_output]) |
| | |
| | with gr.Column(): |
| | summary_btn = gr.Button("Get Data Summary") |
| | summary_output = gr.Textbox(label="Data Summary", lines=10) |
| | summary_btn.click(test_get_summary, outputs=[summary_output]) |
| | |
| | |
| | demo.mcp_functions = { |
| | "download_and_parse_part": { |
| | "function": download_and_parse_part, |
| | "description": "Download and automatically parse all VDI files for a part", |
| | "parameters": { |
| | "type": "object", |
| | "properties": { |
| | "part": { |
| | "type": "integer", |
| | "description": "Part number (e.g., 22 for heat pumps)" |
| | } |
| | }, |
| | "required": ["part"] |
| | } |
| | }, |
| | "search_heatpump": { |
| | "function": search_heatpump, |
| | "description": "Search for specific heat pump by criteria", |
| | "parameters": { |
| | "type": "object", |
| | "properties": { |
| | "manufacturer": { |
| | "type": "string", |
| | "description": "Manufacturer name (optional)" |
| | }, |
| | "product_name": { |
| | "type": "string", |
| | "description": "Product name or partial match (optional)" |
| | }, |
| | "article_number": { |
| | "type": "string", |
| | "description": "Article number (optional)" |
| | }, |
| | "heating_power_min": { |
| | "type": "number", |
| | "description": "Minimum heating power in kW (optional)" |
| | }, |
| | "heating_power_max": { |
| | "type": "number", |
| | "description": "Maximum heating power in kW (optional)" |
| | }, |
| | "heat_pump_type": { |
| | "type": "string", |
| | "description": "Heat pump type (e.g., 'Luft-Wasser') (optional)" |
| | } |
| | } |
| | } |
| | }, |
| | "get_heatpump_details": { |
| | "function": get_heatpump_details, |
| | "description": "Get detailed information about a specific heat pump", |
| | "parameters": { |
| | "type": "object", |
| | "properties": { |
| | "article_number": { |
| | "type": "string", |
| | "description": "Article number of the heat pump" |
| | } |
| | }, |
| | "required": ["article_number"] |
| | } |
| | }, |
| | "list_manufacturers": { |
| | "function": list_manufacturers, |
| | "description": "List all available manufacturers in parsed data", |
| | "parameters": { |
| | "type": "object", |
| | "properties": {} |
| | } |
| | }, |
| | "get_data_summary": { |
| | "function": get_data_summary, |
| | "description": "Get summary of all parsed heat pump data", |
| | "parameters": { |
| | "type": "object", |
| | "properties": {} |
| | } |
| | } |
| | } |
| | |
| | return demo |
| |
|
| | |
| | if __name__ == "__main__": |
| | |
| | demo = create_interface() |
| | |
| | |
| | demo.launch( |
| | server_name="0.0.0.0", |
| | server_port=7860, |
| | share=True, |
| | mcp_server=True |
| | ) |
| |
|