char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
42f0225fe
char/aliceai-t5-moe
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# type: ignore[reportUnusedImport] 5 6import subprocess 7import os 8 9TMP_DIR = os .path .join (os .path .dirname (os .path .abspath (__file__ )),"tmp" ) 10import re 11import json 12from json import JSONDecodeError 13import sys 14import requests 15import time 16from concurrent .futures import ThreadPoolExecutor ,as_completed 17from typing import ( 18Any , 19Callable , 20ContextManager , 21Iterable , 22Iterator , 23List , 24Literal , 25Tuple , 26Set , 27) 28from re import RegexFlag 29import wget 30 31 32DEFAULT_HTTP_TIMEOUT = 60 33 34# per-request timeout, a hung server fails the test instead of stalling the CI for hours 35DEFAULT_REQUEST_TIMEOUT = 600 36 37 38class ServerResponse : 39headers :dict 40status_code :int 41body :dict | Any 42 43 44class ServerError (Exception ): 45def __init__ (self ,code ,body ): 46self .code = code 47self .body = body 48 49 50class ServerProcess : 51# default options 52debug :bool = False 53server_port :int = 8080 54server_host :str = "127.0.0.1" 55model_hf_repo :str | None = "ggml-org/models" 56model_hf_file :str | None = "tinyllamas/stories260K.gguf" 57model_alias :str = "tinyllama-2" 58temperature :float = 0.8 59seed :int = 42 60offline :bool = False 61 62# custom options 63model_alias :str | None = None 64model_tags :str | None = None 65model_url :str | None = None 66model_file :str | None = None 67model_draft :str | None = None 68n_threads :int | None = None 69n_gpu_layer :int | None = None 70n_batch :int | None = None 71n_ubatch :int | None = None 72n_ctx :int | None = None 73n_ga :int | None = None 74n_ga_w :int | None = None 75n_predict :int | None = None 76n_prompts :int | None = 0 77slot_save_path :str | None = None 78id_slot :int | None = None 79cache_prompt :bool | None = None 80n_slots :int | None = None 81ctk :str | None = None 82ctv :str | None = None 83fa :str | None = None 84server_continuous_batching :bool | None = False 85server_embeddings :bool | None = False 86server_reranking :bool | None = False 87server_metrics :bool | None = False 88kv_unified :bool | None = False 89swa_full :bool | None = False 90server_slots :bool | None = False 91pooling :str | None = None 92api_key :str | None = None 93models_dir :str | None = None 94models_max :int | None = None 95models_preset :str | None = None 96no_models_autoload :bool | None = None 97lora_files :List [str ]| None = None 98enable_ctx_shift :int | None = False 99spec_type :str | None = None 100spec_draft_n_min :int | None = None 101spec_draft_n_max :int | None = None 102spec_synth_len :float | None = None 103spec_synth_rates :List [float ]| None = None 104no_ui :bool | None = None 105jinja :bool | None = None 106reasoning_format :Literal ['deepseek' ,'none' ,'nothink' ]| None = None 107reasoning :Literal ['on' ,'off' ,'auto' ]| None = None 108chat_template :str | None = None 109chat_template_file :str | None = None 110server_path :str | None = None 111mmproj_url :str | None = None 112no_mmproj :bool | None = None 113media_path :str | None = None 114sleep_idle_seconds :int | None = None 115cache_ram :int | None = None 116no_cache_idle_slots :bool = False 117log_path :str | None = None 118ui_mcp_proxy :bool = False 119backend_sampling :bool = False 120gcp_compat :bool = False 121server_tools :str | None = None 122server_tools_runtime :str | None = None 123mcp_servers_config :str | None = None 124mcp_servers_json :str | None = None 125cors_origins :str | None = None 126 127# session variables 128process :subprocess .Popen | None = None 129 130def __init__ (self ): 131if "N_GPU_LAYERS" in os .environ : 132self .n_gpu_layer = int (os .environ ["N_GPU_LAYERS" ]) 133if "DEBUG" in os .environ : 134self .debug = True 135if "PORT" in os .environ : 136self .server_port = int (os .environ ["PORT" ]) 137self .external_server = "DEBUG_EXTERNAL" in os .environ 138 139def start (self ,timeout_seconds :int = DEFAULT_HTTP_TIMEOUT )-> None : 140env = { 141** os .environ , 142"LLAMA_SERVER_DEBUG_FAKE_TIMING" :"1" , 143 } 144if "LLAMA_CACHE" not in os .environ : 145env ["LLAMA_CACHE" ]= "tmp" 146if self .external_server : 147f"[external_server]: Assuming external server running on { self . server_host } : { self . server_port } " ) 148return 149if self .server_path is not None : 150server_path = self .server_path 151elif "LLAMA_SERVER_BIN_PATH" in os .environ : 152server_path = os .environ ["LLAMA_SERVER_BIN_PATH" ] 153elif os .name == "nt" : 154server_path = "../../../build/bin/Release/llama-server.exe" 155else : 156server_path = "../../../build/bin/llama-server" 157server_args = [ 158"--host" , 159self .server_host , 160"--port" , 161self .server_port , 162"--temp" , 163self .temperature , 164"--seed" , 165self .seed , 166 ] 167if self .offline : 168server_args .append ("--offline" ) 169if self .model_file : 170server_args .extend (["--model" ,self .model_file ]) 171if self .model_url : 172server_args .extend (["--model-url" ,self .model_url ]) 173if self .model_draft : 174server_args .extend (["--model-draft" ,self .model_draft ]) 175if self .model_hf_repo : 176server_args .extend (["--hf-repo" ,self .model_hf_repo ]) 177if self .model_hf_file : 178server_args .extend (["--hf-file" ,self .model_hf_file ]) 179if self .models_dir : 180server_args .extend (["--models-dir" ,self .models_dir ]) 181if self .models_max is not None : 182server_args .extend (["--models-max" ,self .models_max ]) 183if self .models_preset : 184server_args .extend (["--models-preset" ,self .models_preset ]) 185if self .cors_origins : 186server_args .extend (["--cors-origins" ,self .cors_origins ]) 187if self .n_batch : 188server_args .extend (["--batch-size" ,self .n_batch ]) 189if self .n_ubatch : 190server_args .extend (["--ubatch-size" ,self .n_ubatch ]) 191if self .n_threads : 192server_args .extend (["--threads" ,self .n_threads ]) 193if self .n_gpu_layer : 194server_args .extend (["--n-gpu-layers" ,self .n_gpu_layer ]) 195if self .server_continuous_batching : 196server_args .append ("--cont-batching" ) 197if self .server_embeddings : 198server_args .append ("--embedding" ) 199if self .server_reranking : 200server_args .append ("--reranking" ) 201if self .server_metrics : 202server_args .append ("--metrics" ) 203if self .kv_unified : 204server_args .append ("--kv-unified" ) 205if self .swa_full : 206server_args .append ("--swa-full" ) 207if self .server_slots : 208server_args .append ("--slots" ) 209else : 210server_args .append ("--no-slots" ) 211if self .pooling : 212server_args .extend (["--pooling" ,self .pooling ]) 213if self .model_alias : 214server_args .extend (["--alias" ,self .model_alias ]) 215if self .model_tags : 216server_args .extend (["--tags" ,self .model_tags ]) 217if self .n_ctx : 218server_args .extend (["--ctx-size" ,self .n_ctx ]) 219if self .n_slots : 220server_args .extend (["--parallel" ,self .n_slots ]) 221if self .ctk : 222server_args .extend (["-ctk" ,self .ctk ]) 223if self .ctv : 224server_args .extend (["-ctv" ,self .ctv ]) 225if self .fa is not None : 226server_args .extend (["-fa" ,self .fa ]) 227if self .n_predict : 228server_args .extend (["--n-predict" ,self .n_predict ]) 229if self .slot_save_path : 230server_args .extend (["--slot-save-path" ,self .slot_save_path ]) 231if self .n_ga : 232server_args .extend (["--grp-attn-n" ,self .n_ga ]) 233if self .n_ga_w : 234server_args .extend (["--grp-attn-w" ,self .n_ga_w ]) 235if self .debug : 236server_args .append ("--verbose" ) 237if self .lora_files : 238for lora_file in self .lora_files : 239server_args .extend (["--lora" ,lora_file ]) 240if self .enable_ctx_shift : 241server_args .append ("--context-shift" ) 242if self .spec_type : 243server_args .extend (["--spec-type" ,self .spec_type ]) 244if self .api_key : 245server_args .extend (["--api-key" ,self .api_key ]) 246if self .spec_draft_n_max : 247server_args .extend (["--spec-draft-n-max" ,self .spec_draft_n_max ]) 248if self .spec_draft_n_min : 249server_args .extend (["--spec-draft-n-min" ,self .spec_draft_n_min ]) 250if self .spec_synth_len is not None : 251server_args .extend (["--spec-synth-len" ,self .spec_synth_len ]) 252if self .spec_synth_rates is not None : 253rates = "," .join (str (rate )for rate in self .spec_synth_rates ) 254server_args .extend (["--spec-synth-rates" ,rates ]) 255if self .no_ui : 256server_args .append ("--no-ui" ) 257if self .no_models_autoload : 258server_args .append ("--no-models-autoload" ) 259if self .jinja : 260server_args .append ("--jinja" ) 261else : 262server_args .append ("--no-jinja" ) 263if self .reasoning_format is not None : 264server_args .extend (("--reasoning-format" ,self .reasoning_format )) 265if self .reasoning is not None : 266server_args .extend (("--reasoning" ,self .reasoning )) 267if self .chat_template : 268server_args .extend (["--chat-template" ,self .chat_template ]) 269if self .chat_template_file : 270server_args .extend (["--chat-template-file" ,self .chat_template_file ]) 271if self .mmproj_url : 272server_args .extend (["--mmproj-url" ,self .mmproj_url ]) 273if self .no_mmproj : 274server_args .append ("--no-mmproj" ) 275if self .media_path : 276server_args .extend (["--media-path" ,self .media_path ]) 277if self .sleep_idle_seconds is not None : 278server_args .extend (["--sleep-idle-seconds" ,self .sleep_idle_seconds ]) 279if self .cache_ram is not None : 280server_args .extend (["--cache-ram" ,self .cache_ram ]) 281if self .no_cache_idle_slots : 282server_args .append ("--no-cache-idle-slots" ) 283if self .ui_mcp_proxy : 284server_args .append ("--ui-mcp-proxy" ) 285if self .server_tools : 286server_args .extend (["--tools" ,self .server_tools ]) 287if self .server_tools_runtime : 288server_args .extend (["--tools-runtime" ,self .server_tools_runtime ]) 289if self .mcp_servers_config : 290server_args .extend (["--mcp-servers-config" ,self .mcp_servers_config ]) 291if self .mcp_servers_json : 292server_args .extend (["--mcp-servers-json" ,self .mcp_servers_json ]) 293if self .backend_sampling : 294server_args .append ("--backend_sampling" ) 295if self .gcp_compat : 296env ["AIP_MODE" ]= "PREDICTION" 297env ["AIP_HTTP_PORT" ]= str (self .server_port ) 298 299args = [str (arg )for arg in [server_path ,* server_args ]] 300f"tests: starting server with: { ' ' . join ( args ) } " ) 301 302flags = 0 303if "nt" == os .name : 304flags |= subprocess .DETACHED_PROCESS 305flags |= subprocess .CREATE_NEW_PROCESS_GROUP 306flags |= subprocess .CREATE_NO_WINDOW 307 308if self .log_path : 309self ._log = open (self .log_path ,"w" ) 310else : 311self ._log = sys .stdout 312 313self .process = subprocess .Popen ( 314 [str (arg )for arg in [server_path ,* server_args ]], 315creationflags = flags , 316stdout = self ._log , 317stderr = self ._log if self ._log != sys .stdout else sys .stdout , 318env = env , 319 ) 320server_instances .add (self ) 321 322f"server pid= { self . process . pid } , pytest pid= { os . getpid () } " ) 323 324# wait for server to start 325start_time = time .time () 326last_print_time = start_time 327while time .time ()- start_time < timeout_seconds : 328try : 329response = self .make_request ("GET" ,"/health" ,headers = { 330"Authorization" :f"Bearer { self . api_key } " if self .api_key else None 331 }) 332if response .status_code == 200 : 333self .ready = True 334return # server is ready 335except Exception as e : 336pass 337# Check if process died 338if self .process .poll ()is not None : 339raise RuntimeError (f"Server process died with return code { self . process . returncode } " ) 340 341if time .time ()- last_print_time >= 1.0 : 342f"Waiting for server to start..." ) 343last_print_time = time .time () 344time .sleep (0.01 ) 345raise TimeoutError (f"Server did not start within { timeout_seconds } seconds" ) 346 347def stop (self )-> None : 348if self .external_server : 349"[external_server]: Not stopping external server" ) 350return 351if self in server_instances : 352server_instances .remove (self ) 353if self .process : 354f"Stopping server with pid= { self . process . pid } " ) 355self .process .terminate () 356try : 357self .process .wait (timeout = 5 ) 358except subprocess .TimeoutExpired : 359f"Server pid= { self . process . pid } did not terminate in time, killing" ) 360self .process .kill () 361self .process .wait (timeout = 5 ) 362except Exception as e : 363f"Error waiting for server: { e } " ) 364self .process = None 365if hasattr (self ,'_log' )and self ._log != sys .stdout : 366self ._log .close () 367 368def make_request ( 369self , 370method :str , 371path :str , 372data :dict | Any | None = None , 373headers :dict | None = None , 374timeout :float | None = DEFAULT_REQUEST_TIMEOUT , 375 )-> ServerResponse : 376url = f"http:// { self . server_host } : { self . server_port } { path } " 377parse_body = False 378if method == "GET" : 379response = requests .get (url ,headers = headers ,timeout = timeout ) 380parse_body = True 381elif method == "POST" : 382response = requests .post (url ,headers = headers ,json = data ,timeout = timeout ) 383parse_body = True 384elif method == "DELETE" : 385response = requests .delete (url ,headers = headers ,timeout = timeout ) 386parse_body = True 387elif method == "OPTIONS" : 388response = requests .options (url ,headers = headers ,timeout = timeout ) 389else : 390raise ValueError (f"Unimplemented method: { method } " ) 391result = ServerResponse () 392result .headers = dict (response .headers ) 393result .status_code = response .status_code 394if parse_body : 395try : 396result .body = response .json () 397except (JSONDecodeError ,requests .exceptions .JSONDecodeError ): 398result .body = response .text 399else : 400result .body = None 401"Response from server" ,json .dumps (result .body ,indent = 2 )) 402return result 403 404def make_stream_request ( 405self , 406method :str , 407path :str , 408data :dict | None = None , 409headers :dict | None = None , 410 )-> Iterator [dict ]: 411url = f"http:// { self . server_host } : { self . server_port } { path } " 412if method == "POST" : 413response = requests .post (url ,headers = headers ,json = data ,stream = True ) 414else : 415raise ValueError (f"Unimplemented method: { method } " ) 416if response .status_code != 200 : 417raise ServerError (response .status_code ,response .json ()) 418for line_bytes in response .iter_lines (): 419line = line_bytes .decode ("utf-8" ) 420if '[DONE]' in line : 421break 422elif line .startswith ('data: ' ): 423data = json .loads (line [6 :]) 424"Partial response from server" ,json .dumps (data ,indent = 2 )) 425yield data 426 427def make_any_request ( 428self , 429method :str , 430path :str , 431data :dict | None = None , 432headers :dict | None = None , 433timeout :float | None = DEFAULT_REQUEST_TIMEOUT , 434 )-> dict : 435stream = data .get ('stream' ,False ) 436if stream : 437content :list [str ]= [] 438reasoning_content :list [str ]= [] 439tool_calls :list [dict ]= [] 440finish_reason :Optional [str ]= None 441 442content_parts = 0 443reasoning_content_parts = 0 444tool_call_parts = 0 445arguments_parts = 0 446 447for chunk in self .make_stream_request (method ,path ,data ,headers ): 448if chunk ['choices' ]: 449assert len (chunk ['choices' ])== 1 ,f'Expected 1 choice, got { len ( chunk [ "choices" ]) } ' 450choice = chunk ['choices' ][0 ] 451if choice ['delta' ].get ('content' )is not None : 452assert len (choice ['delta' ]['content' ])> 0 ,f'Expected non empty content delta!' 453content .append (choice ['delta' ]['content' ]) 454content_parts += 1 455if choice ['delta' ].get ('reasoning_content' )is not None : 456assert len (choice ['delta' ]['reasoning_content' ])> 0 ,f'Expected non empty reasoning_content delta!' 457reasoning_content .append (choice ['delta' ]['reasoning_content' ]) 458reasoning_content_parts += 1 459if choice ['delta' ].get ('finish_reason' )is not None : 460finish_reason = choice ['delta' ]['finish_reason' ] 461for tc in choice ['delta' ].get ('tool_calls' , []): 462if 'function' not in tc : 463raise ValueError (f"Expected function type, got { tc [ 'type' ] } " ) 464if tc ['index' ]>= len (tool_calls ): 465assert 'id' in tc 466assert tc .get ('type' )== 'function' 467assert 'function' in tc and 'name' in tc ['function' ]and len (tc ['function' ]['name' ])> 0 , \ 468f"Expected function call with name, got { tc . get ( 'function' ) } " 469tool_calls .append (dict ( 470id = "" , 471type = "function" , 472function = dict ( 473name = "" , 474arguments = "" , 475 ) 476 )) 477tool_call = tool_calls [tc ['index' ]] 478if tc .get ('id' )is not None : 479tool_call ['id' ]= tc ['id' ] 480fct = tc ['function' ] 481assert 'id' not in fct ,f"Function call should not have id: { fct } " 482if fct .get ('name' )is not None : 483tool_call ['function' ]['name' ]= tool_call ['function' ].get ('name' ,'' )+ fct ['name' ] 484if fct .get ('arguments' )is not None : 485tool_call ['function' ]['arguments' ]+= fct ['arguments' ] 486arguments_parts += 1 487tool_call_parts += 1 488else : 489# When `include_usage` is True (the default), we expect the last chunk of the stream 490# immediately preceding the `data: [DONE]` message to contain a `choices` field with an empty array 491# and a `usage` field containing the usage statistics (n.b., llama-server also returns `timings` in 492# the last chunk) 493assert 'usage' in chunk ,f"Expected finish_reason in chunk: { chunk } " 494assert 'timings' in chunk ,f"Expected finish_reason in chunk: { chunk } " 495f'Streamed response had { content_parts } content parts, { reasoning_content_parts } reasoning_content parts, { tool_call_parts } tool call parts incl. { arguments_parts } arguments parts' ) 496result = dict ( 497choices = [ 498dict ( 499index = 0 , 500finish_reason = finish_reason , 501message = dict ( 502role = 'assistant' , 503content = '' .join (content )if content else None , 504reasoning_content = '' .join (reasoning_content )if reasoning_content else None , 505tool_calls = tool_calls if tool_calls else None , 506 ), 507 ) 508 ], 509 ) 510"Final response from server" ,json .dumps (result ,indent = 2 )) 511return result 512else : 513response = self .make_request (method ,path ,data ,headers ,timeout = timeout ) 514assert response .status_code == 200 ,f"Server returned error: { response . status_code } " 515return response .body 516 517 518 519server_instances :Set [ServerProcess ]= set () 520 521 522class ServerPreset : 523@staticmethod 524def load_all ()-> None : 525""" Load all server presets to ensure model files are cached. """ 526servers :List [ServerProcess ]= [ 527method () 528for name ,method in ServerPreset .__dict__ .items () 529if callable (method )and name != "load_all" 530 ] 531for server in servers : 532server .offline = False 533server .start () 534server .stop () 535 536@staticmethod 537def tinyllama2 ()-> ServerProcess : 538server = ServerProcess () 539server .offline = True # will be downloaded by load_all() 540server .model_hf_repo = "ggml-org/test-model-stories260K" 541server .model_hf_file = None 542server .model_alias = "tinyllama-2" 543server .n_ctx = 512 544server .n_batch = 32 545server .n_slots = 2 546server .n_predict = 64 547server .seed = 42 548return server 549 550@staticmethod 551def bert_bge_small ()-> ServerProcess : 552server = ServerProcess () 553server .offline = True # will be downloaded by load_all() 554server .model_hf_repo = "ggml-org/models" 555server .model_hf_file = "bert-bge-small/ggml-model-f16.gguf" 556server .model_alias = "bert-bge-small" 557server .n_ctx = 512 558server .n_batch = 128 559server .n_ubatch = 128 560server .n_slots = 2 561server .seed = 42 562server .server_embeddings = True 563return server 564 565@staticmethod 566def bert_bge_small_with_fa ()-> ServerProcess : 567server = ServerProcess () 568server .offline = True # will be downloaded by load_all() 569server .model_hf_repo = "ggml-org/models" 570server .model_hf_file = "bert-bge-small/ggml-model-f16.gguf" 571server .model_alias = "bert-bge-small" 572server .n_ctx = 1024 573server .n_batch = 300 574server .n_ubatch = 300 575server .n_slots = 2 576server .fa = "on" 577server .seed = 42 578server .server_embeddings = True 579return server 580 581@staticmethod 582def tinyllama_infill ()-> ServerProcess : 583server = ServerProcess () 584server .offline = True # will be downloaded by load_all() 585server .model_hf_repo = "ggml-org/test-model-stories260K-infill" 586server .model_hf_file = None 587server .model_alias = "tinyllama-infill" 588server .n_ctx = 2048 589server .n_batch = 1024 590server .n_slots = 1 591server .n_predict = 64 592server .temperature = 0.0 593server .seed = 42 594return server 595 596@staticmethod 597def stories15m_moe ()-> ServerProcess : 598server = ServerProcess () 599server .offline = True # will be downloaded by load_all() 600server .model_hf_repo = "ggml-org/stories15M_MOE" 601server .model_hf_file = "stories15M_MOE-F16.gguf" 602server .model_alias = "stories15m-moe" 603server .n_ctx = 2048 604server .n_batch = 1024 605server .n_slots = 1 606server .n_predict = 64 607server .temperature = 0.0 608server .seed = 42 609return server 610 611@staticmethod 612def jina_reranker_tiny ()-> ServerProcess : 613server = ServerProcess () 614server .offline = True # will be downloaded by load_all() 615server .model_hf_repo = "ggml-org/models" 616server .model_hf_file = "jina-reranker-v1-tiny-en/ggml-model-f16.gguf" 617server .model_alias = "jina-reranker" 618server .n_ctx = 512 619server .n_batch = 512 620server .n_slots = 1 621server .seed = 42 622server .server_reranking = True 623return server 624 625@staticmethod 626def tinygemma3 ()-> ServerProcess : 627server = ServerProcess () 628server .offline = True # will be downloaded by load_all() 629# mmproj is already provided by HF registry API 630server .model_hf_file = None 631server .model_hf_repo = "ggml-org/tinygemma3-GGUF:Q8_0" 632server .model_alias = "tinygemma3" 633server .n_ctx = 1024 634server .n_batch = 512 635server .n_slots = 2 636server .n_predict = 4 637server .seed = 42 638return server 639 640@staticmethod 641def router ()-> ServerProcess : 642server = ServerProcess () 643server .offline = True # will be downloaded by load_all() 644# router server has no models 645server .model_file = None 646server .model_alias = None 647server .model_hf_repo = None 648server .model_hf_file = None 649server .n_ctx = 1024 650server .n_batch = 16 651server .n_slots = 1 652server .n_predict = 16 653server .seed = 42 654return server 655 656 657def parallel_function_calls (function_list :List [Tuple [Callable [...,Any ],Tuple [Any , ...]]])-> List [Any ]: 658""" 659Run multiple functions in parallel and return results in the same order as calls. Equivalent to Promise.all in JS. 660 661Example usage: 662 663results = parallel_function_calls([ 664(func1, (arg1, arg2)), 665(func2, (arg3, arg4)), 666]) 667""" 668results = [None ]* len (function_list ) 669exceptions = [] 670 671def worker (index ,func ,args ): 672try : 673result = func (* args ) 674results [index ]= result 675except Exception as e : 676exceptions .append ((index ,str (e ))) 677 678with ThreadPoolExecutor ()as executor : 679futures = [] 680for i , (func ,args )in enumerate (function_list ): 681future = executor .submit (worker ,i ,func ,args ) 682futures .append (future ) 683 684# Wait for all futures to complete 685for future in as_completed (futures ): 686pass 687 688# Check if there were any exceptions 689if exceptions : 690"Exceptions occurred:" ) 691for index ,error in exceptions : 692f"Function at index { index } : { error } " ) 693 694return results 695 696 697def match_regex (regex :str ,text :str )-> bool : 698return ( 699re .compile ( 700regex ,flags = RegexFlag .IGNORECASE | RegexFlag .MULTILINE | RegexFlag .DOTALL 701 ).search (text ) 702is not None 703 ) 704 705 706def download_file (url :str ,output_file_path :str | None = None )-> str : 707""" 708Download a file from a URL to a local path. If the file already exists, it will not be downloaded again. 709 710output_file_path is the local path to save the downloaded file. If not provided, the file will be saved in the root directory. 711 712Returns the local path of the downloaded file. 713""" 714file_name = url .split ('/' ).pop () 715output_file = f'./tmp/ { file_name } ' if output_file_path is None else output_file_path 716if not os .path .exists (output_file ): 717f"Downloading { url } to { output_file } " ) 718wget .download (url ,out = output_file ) 719f"Done downloading to { output_file } " ) 720else : 721f"File already exists at { output_file } " ) 722return output_file 723 724 725def is_slow_test_allowed (): 726return os .environ .get ("SLOW_TESTS" )== "1" or os .environ .get ("SLOW_TESTS" )== "ON"