""" Gradio app for Clinical Trial Matcher For Hugging Face Spaces deployment """ import gradio as gr import requests import re import os import html from typing import Tuple, Optional # ClinicalTrials.gov API base URL CLINICALTRIALS_API_BASE = "https://clinicaltrials.gov/api/v2/studies" APP_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); :root { --ctm-bg: #f4f7ff; --ctm-card-bg: #ffffff; --ctm-card-border: #dfe7ff; --ctm-text-primary: #111827; --ctm-text-secondary: #4b5563; --ctm-link: #4f46e5; --ctm-accent-soft: #e0e7ff; } .dark, [data-theme="dark"] { --ctm-bg: #0f172a; --ctm-card-bg: #111827; --ctm-card-border: #334155; --ctm-text-primary: #f8fafc; --ctm-text-secondary: #cbd5e1; --ctm-link: #93c5fd; --ctm-accent-soft: #1e293b; } .ctm-results { color: var(--ctm-text-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .ctm-count { margin: 0.5rem 0 1rem 0; color: var(--ctm-text-primary); font-size: 1.45rem; font-weight: 700; letter-spacing: 0.2px; } .ctm-banner { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 60%, #db2777 100%); color: #ffffff; padding: 1rem 1.1rem; border-radius: 14px; margin: 1rem 0; box-shadow: 0 8px 24px rgba(79, 70, 229, 0.25); } .ctm-info { margin: 1rem 0; padding: 0.9rem 1rem; background: var(--ctm-accent-soft); color: var(--ctm-text-primary); border-left: 4px solid #6366f1; border-radius: 10px; } .ctm-warning { background: #fff7e6; color: #7a4600; padding: 1rem; border-radius: 12px; margin: 1rem 0; border: 1px solid #f2c97d; } .dark .ctm-warning, [data-theme="dark"] .ctm-warning { background: #3a2a0d; color: #fde68a; border-color: #c08425; } .ctm-card { border: 1px solid var(--ctm-card-border); border-radius: 16px; padding: 1.25rem; margin: 1rem 0; background: var(--ctm-card-bg); color: var(--ctm-text-primary); box-shadow: 0 6px 20px rgba(2, 6, 23, 0.08); } .ctm-title { margin: 0; color: var(--ctm-text-primary); flex: 1; font-weight: 700; } .ctm-row { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; } .ctm-meta { color: var(--ctm-text-primary); margin: 0.4rem 0; } .ctm-summary { color: var(--ctm-text-secondary); line-height: 1.65; } .ctm-details { margin-top: 1rem; } .ctm-details summary { cursor: pointer; color: var(--ctm-link); font-weight: 600; } .ctm-reason { margin-top: 1rem; padding: 0.8rem 0.9rem; background: var(--ctm-accent-soft); border-left: 4px solid #6366f1; border-radius: 8px; font-size: 0.92rem; color: var(--ctm-text-primary); } .ctm-footer { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--ctm-card-border); display: flex; justify-content: space-between; align-items: center; } .ctm-muted { color: var(--ctm-text-secondary); font-size: 0.9rem; } .ctm-link { color: var(--ctm-link); text-decoration: none; font-weight: 600; } .ctm-relevance { display: inline-block; margin: 0.3rem 0 0.8rem; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: #fff; padding: 0.26rem 0.65rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; } .ctm-match-highlight { background: #fde68a; color: #1f2937; border-radius: 4px; padding: 0 0.1rem; } .ctm-match-box { margin: 0.8rem 0 0.9rem; padding: 0.75rem 0.85rem; background: var(--ctm-accent-soft); border-left: 4px solid #6366f1; border-radius: 8px; font-size: 0.9rem; color: var(--ctm-text-primary); } .ctm-match-pill { display: inline-block; margin: 0 0.3rem 0.32rem 0; padding: 0.16rem 0.52rem; background: #e0e7ff; color: #3730a3; border-radius: 999px; font-size: 0.75rem; font-weight: 600; } .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; background: radial-gradient(circle at top right, #eef2ff 0%, #f7f9ff 45%, #f4f7ff 100%) !important; color: var(--ctm-text-primary) !important; } .gradio-container .main { max-width: 1180px !important; margin: 0 auto !important; } .gradio-container .block { border-radius: 16px !important; } .gradio-container .gr-box, .gradio-container [class*="panel"] { border-radius: 14px !important; border: 1px solid #e3e8ff !important; box-shadow: 0 8px 24px rgba(34, 46, 120, 0.08) !important; background: var(--ctm-card-bg) !important; color: var(--ctm-text-primary) !important; } .dark .gradio-container .gr-box, .dark .gradio-container [class*="panel"], [data-theme="dark"] .gradio-container .gr-box, [data-theme="dark"] .gradio-container [class*="panel"] { background: #111827 !important; border-color: #334155 !important; color: #f8fafc !important; } .dark .gradio-container, [data-theme="dark"] .gradio-container { background: radial-gradient(circle at top right, #111827 0%, #0f172a 55%, #0b1220 100%) !important; } .gradio-container button { border-radius: 12px !important; font-weight: 600 !important; letter-spacing: 0.01em !important; transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease !important; } .gradio-container button:hover { transform: translateY(-1px) !important; box-shadow: 0 10px 20px rgba(76, 70, 229, 0.22) !important; filter: brightness(1.02) !important; } .gradio-container input, .gradio-container textarea, .gradio-container select { border-radius: 12px !important; border: 1px solid #dbe4ff !important; box-shadow: none !important; background: var(--ctm-card-bg) !important; color: var(--ctm-text-primary) !important; } .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus { border-color: #6366f1 !important; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15) !important; } .gradio-container h1 { letter-spacing: -0.02em !important; font-weight: 700 !important; } .gradio-container h2, .gradio-container h3 { letter-spacing: -0.01em !important; } .ctm-rank-input textarea, .ctm-rank-input input { border-radius: 10px !important; border: 1px solid #dbe4ff !important; } .ctm-rank-btn button { background: linear-gradient(135deg, #5b6cf9 0%, #7a56da 100%) !important; border: none !important; border-radius: 10px !important; font-weight: 600 !important; } .ctm-rank-panel { margin-top: 0.75rem; padding: 0.9rem; border: 1px solid #dbe4ff; border-radius: 12px; background: linear-gradient(180deg, #f8faff 0%, #f3f6ff 100%); } .ctm-rank-help { margin-bottom: 0.55rem; color: #34435f; font-size: 0.9rem; line-height: 1.45; } .dark .ctm-rank-panel, [data-theme="dark"] .ctm-rank-panel { border-color: #334155; background: linear-gradient(180deg, #111827 0%, #182133 100%); } .dark .ctm-rank-help, [data-theme="dark"] .ctm-rank-help { color: #dbe6ff; } .dark .ctm-rank-input textarea, .dark .ctm-rank-input input, [data-theme="dark"] .ctm-rank-input textarea, [data-theme="dark"] .ctm-rank-input input { border-color: #334155 !important; background: #111827 !important; color: #f8fafc !important; } """ def _highlight_html(text: str, terms: list) -> str: """Highlight matched terms in plain text and return safe HTML.""" safe_text = html.escape(text or "") valid_terms = sorted({(t or "").strip() for t in (terms or []) if (t or "").strip()}, key=len, reverse=True) if not safe_text or not valid_terms: return safe_text highlighted = safe_text for term in valid_terms: safe_term = html.escape(term) if len(safe_term) < 2: continue pattern = re.compile(re.escape(safe_term), re.IGNORECASE) highlighted = pattern.sub(lambda m: f"{m.group(0)}", highlighted) return highlighted # Import LLM service (optional - will gracefully degrade if not available) LLM_AVAILABLE = False get_llm_service = None # Check environment variables USE_HF_API = os.environ.get('USE_HF_API', 'false').lower() == 'true' HF_TOKEN = os.environ.get('HUGGINGFACE_API_TOKEN', '') print(f"Environment check - USE_HF_API: {USE_HF_API}, HF_TOKEN set: {bool(HF_TOKEN)}") try: from llm_service import get_llm_service LLM_AVAILABLE = True print("LLM service imported successfully.") except ImportError as e: print(f"LLM service not available. Ranking will be disabled. Error: {str(e)}") except Exception as e: print(f"Error importing LLM service: {str(e)}") import traceback traceback.print_exc() def search_trials(query: str, country: str, status: str, ranking_terms: str = "") -> Tuple[str, int, Optional[str]]: """ Search clinical trials and return formatted results Returns: tuple: (results_html, total_count, llm_model_name) """ if not query: return "Please enter search keywords.", 0, None try: # Build API request parameters params = { 'format': 'json', 'pageSize': 20 } # Build query filter query_parts = [] if query: query_parts.append(query) if country: query_parts.append(f'AREA[LocationCountry]{country}') if status: query_parts.append(f'AREA[OverallStatus]{status}') if query_parts: params['query.term'] = ' AND '.join(query_parts) # Make request to ClinicalTrials.gov API response = requests.get(CLINICALTRIALS_API_BASE, params=params, timeout=30) response.raise_for_status() data = response.json() # Extract and format relevant information studies = [] if 'studies' in data: for study in data['studies']: protocol_section = study.get('protocolSection', {}) identification = protocol_section.get('identificationModule', {}) nct_id = identification.get('nctId', '') organization = identification.get('organization', {}) sponsor = organization.get('fullName', '') if organization else '' status_module = protocol_section.get('statusModule', {}) description = protocol_section.get('descriptionModule', {}) conditions = protocol_section.get('conditionsModule', {}) locations_module = protocol_section.get('contactsLocationsModule', {}) # Fetch full study details to get ALL locations and eligibility criteria locations = [] inclusion_criteria = [] exclusion_criteria = [] detail_data = None if nct_id: try: detail_url = f"https://clinicaltrials.gov/api/v2/studies/{nct_id}" detail_response = requests.get(detail_url, params={'format': 'json'}, timeout=60) detail_response.raise_for_status() detail_data = detail_response.json() detail_protocol = detail_data.get('protocolSection', {}) detail_locations_module = detail_protocol.get('contactsLocationsModule', {}) if 'locations' in detail_locations_module: all_locations = detail_locations_module['locations'] if isinstance(all_locations, list) and len(all_locations) > 0: for loc in all_locations: locations.append({ 'facility': loc.get('facility', ''), 'city': loc.get('city', ''), 'country': loc.get('country', '') }) except Exception: pass # Extract eligibility criteria from detail data if available if detail_data: detail_protocol = detail_data.get('protocolSection', {}) eligibility_module = detail_protocol.get('eligibilityModule', {}) eligibility_text = eligibility_module.get('eligibilityCriteria', '') if eligibility_text: # Parse inclusion and exclusion criteria with multiple format support inclusion_patterns = [ r'(?:Key\s+)?Inclusion\s+Criteria[^:\n]*:?\s*(.*?)(?=(?:Key\s+)?Exclusion\s+Criteria[^:\n]*:?|$)', r'Inclusion\s+Criteria[^:\n]*:?\s*(.*?)(?=Exclusion\s+Criteria[^:\n]*:?|$)', ] exclusion_patterns = [ r'(?:Key\s+)?Exclusion\s+Criteria[^:\n]*:?\s*(.*?)$', r'Exclusion\s+Criteria[^:\n]*:?\s*(.*?)$', ] inclusion_match = None exclusion_match = None for pattern in inclusion_patterns: inclusion_match = re.search(pattern, eligibility_text, re.IGNORECASE | re.DOTALL) if inclusion_match: break for pattern in exclusion_patterns: exclusion_match = re.search(pattern, eligibility_text, re.IGNORECASE | re.DOTALL) if exclusion_match: break if inclusion_match: inclusion_text = inclusion_match.group(1).strip() inclusion_lines = [line.strip() for line in inclusion_text.split('\n') if line.strip()] inclusion_criteria = [re.sub(r'^[\*\-\•]\s*', '', line).strip() for line in inclusion_lines if line.strip()] inclusion_criteria = [item for item in inclusion_criteria if item and len(item) > 3] if exclusion_match: exclusion_text = exclusion_match.group(1).strip() exclusion_lines = [line.strip() for line in exclusion_text.split('\n') if line.strip()] exclusion_criteria = [re.sub(r'^[\*\-\•]\s*', '', line).strip() for line in exclusion_lines if line.strip()] exclusion_criteria = [item for item in exclusion_criteria if item and len(item) > 3] else: # Try to get eligibility from search results as fallback eligibility_module = protocol_section.get('eligibilityModule', {}) eligibility_text = eligibility_module.get('eligibilityCriteria', '') if eligibility_text: inclusion_patterns = [ r'(?:Key\s+)?Inclusion\s+Criteria[^:\n]*:?\s*(.*?)(?=(?:Key\s+)?Exclusion\s+Criteria[^:\n]*:?|$)', r'Inclusion\s+Criteria[^:\n]*:?\s*(.*?)(?=Exclusion\s+Criteria[^:\n]*:?|$)', ] exclusion_patterns = [ r'(?:Key\s+)?Exclusion\s+Criteria[^:\n]*:?\s*(.*?)$', r'Exclusion\s+Criteria[^:\n]*:?\s*(.*?)$', ] inclusion_match = None exclusion_match = None for pattern in inclusion_patterns: inclusion_match = re.search(pattern, eligibility_text, re.IGNORECASE | re.DOTALL) if inclusion_match: break for pattern in exclusion_patterns: exclusion_match = re.search(pattern, eligibility_text, re.IGNORECASE | re.DOTALL) if exclusion_match: break if inclusion_match: inclusion_text = inclusion_match.group(1).strip() inclusion_lines = [line.strip() for line in inclusion_text.split('\n') if line.strip()] inclusion_criteria = [re.sub(r'^[\*\-\•]\s*', '', line).strip() for line in inclusion_lines if line.strip()] inclusion_criteria = [item for item in inclusion_criteria if item and len(item) > 3] if exclusion_match: exclusion_text = exclusion_match.group(1).strip() exclusion_lines = [line.strip() for line in exclusion_text.split('\n') if line.strip()] exclusion_criteria = [re.sub(r'^[\*\-\•]\s*', '', line).strip() for line in exclusion_lines if line.strip()] exclusion_criteria = [item for item in exclusion_criteria if item and len(item) > 3] # If detail fetch failed or returned no locations, use search results as fallback if len(locations) == 0 and 'locations' in locations_module: all_locations = locations_module.get('locations', []) if isinstance(all_locations, list): for loc in all_locations: locations.append({ 'facility': loc.get('facility', ''), 'city': loc.get('city', ''), 'country': loc.get('country', '') }) # Sort locations: searched country first, then others if country and locations: search_country_lower = country.lower().strip() def location_sort_key(loc): loc_country = loc.get('country', '').lower().strip() if loc_country == search_country_lower: return 0 elif search_country_lower in loc_country or loc_country in search_country_lower: return 1 else: return 2 locations.sort(key=location_sort_key) study_info = { 'nctId': nct_id, 'title': identification.get('officialTitle') or identification.get('briefTitle', ''), 'sponsor': sponsor, 'status': status_module.get('overallStatus', ''), 'conditions': conditions.get('conditions', []), 'briefSummary': description.get('briefSummary', ''), 'locations': locations, 'inclusionCriteria': inclusion_criteria, 'exclusionCriteria': exclusion_criteria, 'lastUpdateDate': status_module.get('lastUpdateSubmitDate', '') } studies.append(study_info) # Apply LLM-based ranking if ranking terms provided llm_model_name = None if ranking_terms and ranking_terms.strip(): if not LLM_AVAILABLE: print("Warning: LLM service not available. Ranking disabled.") print(f" - LLM_AVAILABLE: {LLM_AVAILABLE}") print(f" - get_llm_service: {get_llm_service}") elif not get_llm_service: print("Warning: get_llm_service is None. Ranking disabled.") else: try: llm_service = get_llm_service() if llm_service: llm_model_name = llm_service.model_name print(f"Ranking studies with {llm_model_name} using terms: {ranking_terms}") studies = llm_service.rank_studies(studies, ranking_terms) print(f"Ranking completed. {len(studies)} studies ranked.") else: print("Warning: LLM service returned None. Ranking disabled.") print(f" - Check environment variables:") print(f" USE_HF_API: {os.environ.get('USE_HF_API', 'NOT SET')}") print(f" HUGGINGFACE_API_TOKEN: {'SET' if os.environ.get('HUGGINGFACE_API_TOKEN') else 'NOT SET'}") except Exception as e: print(f"Error during LLM ranking: {str(e)}") import traceback traceback.print_exc() # Format results as HTML if not studies: return "No studies found. Try different search criteria.", 0, None html_results = f'

Found {len(studies)} studies

' if ranking_terms and ranking_terms.strip() and llm_model_name: html_results += f'
🤖 Results ranked by {llm_model_name}
Ranking terms: "{ranking_terms}"
' html_results += f'

Ranking applied: Studies have been reordered by AI relevance to "{ranking_terms}". The model analyzed each study\'s title, summary, conditions, and inclusion criteria to determine how closely they match your ranking terms. Higher-ranked studies appear first.

' elif ranking_terms and ranking_terms.strip() and not llm_model_name: # Check what's wrong use_hf_api = os.environ.get('USE_HF_API', 'false').lower() == 'true' hf_token = os.environ.get('HUGGINGFACE_API_TOKEN', '') error_msg = "LLM service is not available." if not use_hf_api: error_msg += " Set USE_HF_API=true in your Space secrets." if not hf_token: error_msg += " Set HUGGINGFACE_API_TOKEN in your Space secrets." html_results += f'
⚠️ Ranking not applied: {error_msg}
Check your Space Settings → Secrets to add the required environment variables.
' for idx, study in enumerate(studies): status_badge_color = { 'RECRUITING': '#28a745', 'NOT_YET_RECRUITING': '#ffc107', 'ACTIVE_NOT_RECRUITING': '#17a2b8', 'COMPLETED': '#6c757d', 'SUSPENDED': '#dc3545', 'TERMINATED': '#dc3545', 'WITHDRAWN': '#6c757d' }.get(study['status'], '#6c757d') # Add relevance score indicator if ranking was applied relevance_indicator = "" if 'relevance_score' in study: score = study.get('relevance_score', 0) score_percent = int(score * 100) # Color based on score: green for high, yellow for medium, gray for low if score >= 0.7: score_color = '#28a745' elif score >= 0.4: score_color = '#ffc107' else: score_color = '#6c757d' relevance_indicator = f'Relevance: {score_percent}%' match_terms = study.get("ranking_match_terms", []) title_html = _highlight_html(study.get("title", ""), match_terms) html_results += f"""

{title_html}

{html.escape(study['status'])} {relevance_indicator}

NCT ID: {html.escape(study['nctId'])}

""" if study.get('sponsor'): html_results += f'

Sponsor: {html.escape(study["sponsor"])}

' if study.get('conditions'): conditions_html = ", ".join(_highlight_html(cond, match_terms) for cond in study["conditions"]) html_results += f'

Conditions: {conditions_html}

' if study.get('briefSummary'): summary = study['briefSummary'][:600] + '...' if len(study['briefSummary']) > 600 else study['briefSummary'] html_results += f'

Summary: {_highlight_html(summary, match_terms)}

' if match_terms: html_results += "
Why matched: " html_results += "".join( f"{html.escape(term)}" for term in match_terms[:10] ) field_matches = study.get("ranking_match_fields", {}) if field_matches: html_results += "
" for field, terms in field_matches.items(): term_text = ", ".join(html.escape(t) for t in terms[:6]) html_results += f"
{html.escape(field)}: {term_text}
" html_results += "
" html_results += "
" if study.get('inclusionCriteria'): html_results += "
Inclusion Criteria ▼
    " for criterion in study['inclusionCriteria']: html_results += f"
  • {_highlight_html(criterion, match_terms)}
  • " html_results += "
" if study.get('exclusionCriteria'): html_results += "
Exclusion Criteria ▼
    " for criterion in study['exclusionCriteria']: html_results += f"
  • {_highlight_html(criterion, match_terms)}
  • " html_results += "
" if study.get('ranking_reasoning'): html_results += f"

Ranking Reasoning: {html.escape(study['ranking_reasoning'])}

" html_results += f"""
""" html_results += "
" return html_results, len(studies), llm_model_name except Exception as e: return f"Error: {str(e)}", 0, None # Create Gradio interface with gr.Blocks( title="Clinical Trial Matcher", theme=gr.themes.Soft( primary_hue="indigo", neutral_hue="slate", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], ), css=APP_CSS, ) as demo: # Inject PWA meta tags for iOS/Android installation gr.HTML(""" """, visible=False) gr.Markdown(""" # 🔬 Clinical Trial Matcher Search and filter clinical trials from [ClinicalTrials.gov](https://clinicaltrials.gov/) with AI-powered fine-tuning of the results based on your query. """) with gr.Row(): with gr.Column(scale=1): query_input = gr.Textbox( label="Search Keywords", placeholder="e.g., pancreatic cancer, PDAC, KRAS, etc.", value="" ) country_input = gr.Dropdown( label="Country (Optional)", choices=[ "", "Germany", "United States", "United Kingdom", "Canada", "France", "Italy", "Spain", "Netherlands", "Belgium", "Switzerland", "Austria", "Sweden", "Norway", "Denmark", "Finland", "Poland", "Czech Republic", "Australia", "New Zealand", "Japan", "China", "India", "South Korea", "Brazil", "Mexico", "Argentina", "Chile", "South Africa", "Israel", "Turkey", "Russia", "Greece", "Portugal", "Ireland" ], value="", filterable=True, interactive=True ) status_input = gr.Dropdown( label="Status (Optional)", choices=["", "RECRUITING", "NOT_YET_RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "SUSPENDED", "TERMINATED", "WITHDRAWN"], value="", interactive=True ) search_btn = gr.Button("Search Clinical Trials", variant="primary", size="lg") with gr.Column(scale=2): with gr.Group(visible=False, elem_classes=["ctm-rank-panel"]) as ranking_panel: ranking_help = gr.Markdown( "### ✨ AI Ranking (after search)\n" "1. Enter what matters clinically (biomarker, drug, mechanism, line of therapy).\n" "2. Click **Rank Results** to reorder studies by relevance.\n" "3. Review **Why matched** and highlighted terms in each result.", elem_classes=["ctm-rank-help"], ) ranking_input = gr.Textbox( label="Ranking Query", placeholder="e.g., KRAS G12D, daraxonrasib, immunotherapy", value="", elem_classes=["ctm-rank-input"] ) ranking_btn = gr.Button("Rank Results", variant="secondary", elem_classes=["ctm-rank-btn"]) results_output = gr.HTML(label="Results") count_output = gr.Textbox(label="Total Studies Found", visible=False) # Search function def perform_search(query, country, status, ranking_terms=""): html, count, model = search_trials(query, country, status, ranking_terms) # Show ranking panel after first search return html, count, gr.update(visible=True), gr.update(visible=True) # Ranking function def perform_ranking(query, country, status, ranking_terms): if not ranking_terms or not ranking_terms.strip(): return "Please enter ranking terms to sort the results.", 0 print(f"Ranking requested with terms: {ranking_terms}") html, count, model = search_trials(query, country, status, ranking_terms) return html, count search_btn.click( fn=perform_search, inputs=[query_input, country_input, status_input, ranking_input], outputs=[results_output, count_output, ranking_panel, search_btn] ) ranking_btn.click( fn=perform_ranking, inputs=[query_input, country_input, status_input, ranking_input], outputs=[results_output, count_output] ) gr.Markdown(""" --- **App. developed by [Mackenzie Weygandt Mathis](https://en.wikipedia.org/wiki/Mackenzie_Weygandt_Mathis). Data sourced from [ClinicalTrials.gov](https://clinicaltrials.gov)** This app uses AI-powered ranking with Hugging Face models (default: DeepSeek-V3.2) to intelligently rank search results by relevance. """) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)