opik-optimizer 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opik_optimizer/__init__.py +65 -0
- opik_optimizer/_throttle.py +43 -0
- opik_optimizer/base_optimizer.py +240 -0
- opik_optimizer/cache_config.py +24 -0
- opik_optimizer/demo/__init__.py +7 -0
- opik_optimizer/demo/cache.py +112 -0
- opik_optimizer/demo/datasets.py +656 -0
- opik_optimizer/few_shot_bayesian_optimizer/__init__.py +5 -0
- opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +408 -0
- opik_optimizer/few_shot_bayesian_optimizer/prompt_parameter.py +91 -0
- opik_optimizer/few_shot_bayesian_optimizer/prompt_templates.py +80 -0
- opik_optimizer/integrations/__init__.py +0 -0
- opik_optimizer/logging_config.py +69 -0
- opik_optimizer/meta_prompt_optimizer.py +1100 -0
- opik_optimizer/mipro_optimizer/__init__.py +1 -0
- opik_optimizer/mipro_optimizer/_lm.py +394 -0
- opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +1058 -0
- opik_optimizer/mipro_optimizer/mipro_optimizer.py +395 -0
- opik_optimizer/mipro_optimizer/utils.py +107 -0
- opik_optimizer/optimization_config/__init__.py +0 -0
- opik_optimizer/optimization_config/configs.py +35 -0
- opik_optimizer/optimization_config/mappers.py +49 -0
- opik_optimizer/optimization_result.py +211 -0
- opik_optimizer/task_evaluator.py +102 -0
- opik_optimizer/utils.py +132 -0
- opik_optimizer-0.7.0.dist-info/METADATA +35 -0
- opik_optimizer-0.7.0.dist-info/RECORD +30 -0
- opik_optimizer-0.7.0.dist-info/WHEEL +5 -0
- opik_optimizer-0.7.0.dist-info/licenses/LICENSE +21 -0
- opik_optimizer-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
import importlib.metadata
|
2
|
+
import logging
|
3
|
+
from .logging_config import setup_logging
|
4
|
+
|
5
|
+
__version__ = importlib.metadata.version("opik_optimizer")
|
6
|
+
|
7
|
+
# Using WARNING as a sensible default to avoid flooding users with INFO/DEBUG
|
8
|
+
setup_logging(level=logging.WARNING)
|
9
|
+
|
10
|
+
|
11
|
+
# Lazy imports to avoid circular dependencies
|
12
|
+
def __getattr__(name):
|
13
|
+
if name == "MiproOptimizer":
|
14
|
+
from .mipro_optimizer import MiproOptimizer
|
15
|
+
|
16
|
+
return MiproOptimizer
|
17
|
+
elif name == "BaseOptimizer":
|
18
|
+
from .base_optimizer import BaseOptimizer
|
19
|
+
|
20
|
+
return BaseOptimizer
|
21
|
+
elif name == "MetaPromptOptimizer":
|
22
|
+
from .meta_prompt_optimizer import MetaPromptOptimizer
|
23
|
+
|
24
|
+
return MetaPromptOptimizer
|
25
|
+
elif name == "FewShotBayesianOptimizer":
|
26
|
+
from .few_shot_bayesian_optimizer import FewShotBayesianOptimizer
|
27
|
+
|
28
|
+
return FewShotBayesianOptimizer
|
29
|
+
elif name in ["MetricConfig", "OptimizationConfig", "TaskConfig"]:
|
30
|
+
from .optimization_config.configs import (
|
31
|
+
MetricConfig,
|
32
|
+
OptimizationConfig,
|
33
|
+
TaskConfig,
|
34
|
+
)
|
35
|
+
|
36
|
+
return locals()[name]
|
37
|
+
elif name in ["from_dataset_field", "from_llm_response_text"]:
|
38
|
+
from .optimization_config.mappers import (
|
39
|
+
from_dataset_field,
|
40
|
+
from_llm_response_text,
|
41
|
+
)
|
42
|
+
|
43
|
+
return locals()[name]
|
44
|
+
raise AttributeError(f"module 'opik_optimizer' has no attribute '{name}'")
|
45
|
+
|
46
|
+
|
47
|
+
from opik.evaluation.models.litellm import warning_filters
|
48
|
+
|
49
|
+
warning_filters.add_warning_filters()
|
50
|
+
|
51
|
+
from .optimization_result import OptimizationResult
|
52
|
+
|
53
|
+
__all__ = [
|
54
|
+
"BaseOptimizer",
|
55
|
+
"FewShotBayesianOptimizer",
|
56
|
+
"MetaPromptOptimizer",
|
57
|
+
"MiproOptimizer",
|
58
|
+
"MetricConfig",
|
59
|
+
"OptimizationConfig",
|
60
|
+
"TaskConfig",
|
61
|
+
"from_dataset_field",
|
62
|
+
"from_llm_response_text",
|
63
|
+
"OptimizationResult",
|
64
|
+
"setup_logging",
|
65
|
+
]
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import threading
|
2
|
+
import time
|
3
|
+
import queue
|
4
|
+
from functools import wraps
|
5
|
+
|
6
|
+
class RateLimiter:
|
7
|
+
"""
|
8
|
+
Rate limiter that enforces a maximum number of calls across all threads.
|
9
|
+
"""
|
10
|
+
def __init__(self, max_calls_per_second):
|
11
|
+
self.max_calls_per_second = max_calls_per_second
|
12
|
+
self.interval = 1.0 / max_calls_per_second # Time between allowed calls
|
13
|
+
self.last_call_time = 0
|
14
|
+
self.lock = threading.Lock()
|
15
|
+
|
16
|
+
def acquire(self):
|
17
|
+
"""
|
18
|
+
Wait until a call is allowed according to the global rate limit.
|
19
|
+
Returns immediately if the call is allowed, otherwise blocks until it's time.
|
20
|
+
"""
|
21
|
+
with self.lock:
|
22
|
+
current_time = time.time()
|
23
|
+
time_since_last = current_time - self.last_call_time
|
24
|
+
|
25
|
+
# If we haven't waited long enough since the last call
|
26
|
+
if time_since_last < self.interval:
|
27
|
+
# Calculate how much longer we need to wait
|
28
|
+
sleep_time = self.interval - time_since_last
|
29
|
+
time.sleep(sleep_time)
|
30
|
+
|
31
|
+
# Update the last call time (after potential sleep)
|
32
|
+
self.last_call_time = time.time()
|
33
|
+
|
34
|
+
def rate_limited(limiter):
|
35
|
+
"""Decorator to rate limit a function using the provided limiter"""
|
36
|
+
def decorator(func):
|
37
|
+
@wraps(func)
|
38
|
+
def wrapper(*args, **kwargs):
|
39
|
+
limiter.acquire()
|
40
|
+
return func(*args, **kwargs)
|
41
|
+
return wrapper
|
42
|
+
return decorator
|
43
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
from typing import Optional, Union, List, Dict, Any
|
2
|
+
import opik
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
|
6
|
+
import litellm
|
7
|
+
from opik.evaluation import metrics
|
8
|
+
from opik.opik_context import get_current_span_data
|
9
|
+
from opik.rest_api.core import ApiError
|
10
|
+
|
11
|
+
from pydantic import BaseModel
|
12
|
+
from ._throttle import RateLimiter, rate_limited
|
13
|
+
from .cache_config import initialize_cache
|
14
|
+
from opik.evaluation.models.litellm import opik_monitor as opik_litellm_monitor
|
15
|
+
from .optimization_config.configs import TaskConfig, MetricConfig
|
16
|
+
|
17
|
+
limiter = RateLimiter(max_calls_per_second=15)
|
18
|
+
|
19
|
+
# Don't use unsupported params:
|
20
|
+
litellm.drop_params = True
|
21
|
+
|
22
|
+
# Set up logging:
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
class OptimizationRound(BaseModel):
|
27
|
+
round_number: int
|
28
|
+
current_prompt: str
|
29
|
+
current_score: float
|
30
|
+
generated_prompts: List[Dict[str, Any]]
|
31
|
+
best_prompt: str
|
32
|
+
best_score: float
|
33
|
+
improvement: float
|
34
|
+
|
35
|
+
|
36
|
+
class BaseOptimizer:
|
37
|
+
def __init__(self, model: str, project_name: Optional[str] = None, **model_kwargs):
|
38
|
+
"""
|
39
|
+
Base class for optimizers.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
model: LiteLLM model name
|
43
|
+
project_name: Opik project name
|
44
|
+
model_kwargs: additional args for model (eg, temperature)
|
45
|
+
"""
|
46
|
+
self.model = model
|
47
|
+
self.reasoning_model = model
|
48
|
+
self.model_kwargs = model_kwargs
|
49
|
+
self.project_name = project_name
|
50
|
+
self._history = []
|
51
|
+
self.experiment_config = None
|
52
|
+
self.llm_call_counter = 0
|
53
|
+
|
54
|
+
# Initialize shared cache
|
55
|
+
initialize_cache()
|
56
|
+
|
57
|
+
def optimize_prompt(
|
58
|
+
self,
|
59
|
+
dataset: Union[str, opik.Dataset],
|
60
|
+
metric_config: MetricConfig,
|
61
|
+
task_config: TaskConfig,
|
62
|
+
prompt: str,
|
63
|
+
input_key: str,
|
64
|
+
output_key: str,
|
65
|
+
experiment_config: Optional[Dict] = None,
|
66
|
+
**kwargs,
|
67
|
+
):
|
68
|
+
"""
|
69
|
+
Optimize a prompt.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
dataset: Opik dataset name, or Opik dataset
|
73
|
+
metric_config: instance of a MetricConfig
|
74
|
+
task_config: instance of a TaskConfig
|
75
|
+
prompt: the prompt to optimize
|
76
|
+
input_key: input field of dataset
|
77
|
+
output_key: output field of dataset
|
78
|
+
experiment_config: Optional configuration for the experiment
|
79
|
+
**kwargs: Additional arguments for optimization
|
80
|
+
"""
|
81
|
+
self.dataset = dataset
|
82
|
+
self.metric = metric
|
83
|
+
self.prompt = prompt
|
84
|
+
self.input_key = input_key
|
85
|
+
self.output_key = output_key
|
86
|
+
self.experiment_config = experiment_config
|
87
|
+
|
88
|
+
def evaluate_prompt(
|
89
|
+
self,
|
90
|
+
dataset: Union[str, opik.Dataset],
|
91
|
+
metric_config: MetricConfig,
|
92
|
+
prompt: str,
|
93
|
+
input_key: str,
|
94
|
+
output_key: str,
|
95
|
+
n_samples: int = 10,
|
96
|
+
task_config: Optional[TaskConfig] = None,
|
97
|
+
dataset_item_ids: Optional[List[str]] = None,
|
98
|
+
experiment_config: Optional[Dict] = None,
|
99
|
+
**kwargs,
|
100
|
+
) -> float:
|
101
|
+
"""
|
102
|
+
Evaluate a prompt.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
dataset: Opik dataset name, or Opik dataset
|
106
|
+
metric_config: instance of a MetricConfig
|
107
|
+
task_config: instance of a TaskConfig
|
108
|
+
prompt: the prompt to evaluate
|
109
|
+
input_key: input field of dataset
|
110
|
+
output_key: output field of dataset
|
111
|
+
n_samples: number of items to test in the dataset
|
112
|
+
dataset_item_ids: Optional list of dataset item IDs to evaluate
|
113
|
+
experiment_config: Optional configuration for the experiment
|
114
|
+
**kwargs: Additional arguments for evaluation
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
float: The evaluation score
|
118
|
+
"""
|
119
|
+
self.dataset = dataset
|
120
|
+
self.metric_config = metric_config
|
121
|
+
self.task_config = task_config
|
122
|
+
self.prompt = prompt
|
123
|
+
self.input_key = input_key
|
124
|
+
self.output_key = output_key
|
125
|
+
self.experiment_config = experiment_config
|
126
|
+
return 0.0 # Base implementation returns 0
|
127
|
+
|
128
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
129
|
+
"""
|
130
|
+
Get the optimization history.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
List[Dict[str, Any]]: List of optimization rounds with their details
|
134
|
+
"""
|
135
|
+
return self._history
|
136
|
+
|
137
|
+
def _add_to_history(self, round_data: Dict[str, Any]):
|
138
|
+
"""
|
139
|
+
Add a round to the optimization history.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
round_data: Dictionary containing round details
|
143
|
+
"""
|
144
|
+
self._history.append(round_data)
|
145
|
+
|
146
|
+
@rate_limited(limiter)
|
147
|
+
def _call_model(
|
148
|
+
self,
|
149
|
+
prompt: str,
|
150
|
+
system_prompt: Optional[str] = None,
|
151
|
+
is_reasoning: bool = False,
|
152
|
+
) -> str:
|
153
|
+
"""Call the model to get suggestions based on the meta-prompt."""
|
154
|
+
model = self.reasoning_model if is_reasoning else self.model
|
155
|
+
messages = []
|
156
|
+
|
157
|
+
if system_prompt:
|
158
|
+
messages.append({"role": "system", "content": system_prompt})
|
159
|
+
logger.debug(f"Using custom system prompt: {system_prompt[:100]}...")
|
160
|
+
else:
|
161
|
+
messages.append(
|
162
|
+
{"role": "system", "content": "You are a helpful assistant."}
|
163
|
+
)
|
164
|
+
|
165
|
+
messages.append({"role": "user", "content": prompt})
|
166
|
+
logger.debug(f"Calling model {model} with prompt: {prompt[:100]}...")
|
167
|
+
|
168
|
+
api_params = self.model_kwargs.copy()
|
169
|
+
api_params.update(
|
170
|
+
{
|
171
|
+
"model": model,
|
172
|
+
"messages": messages,
|
173
|
+
# Ensure required params like 'temperature', 'max_tokens' are present
|
174
|
+
# Defaults added here for safety, though usually set in __init__ kwargs
|
175
|
+
"temperature": api_params.get("temperature", 0.3),
|
176
|
+
"max_tokens": api_params.get("max_tokens", 1000),
|
177
|
+
}
|
178
|
+
)
|
179
|
+
|
180
|
+
# Attempt to add Opik monitoring if available
|
181
|
+
try:
|
182
|
+
# Assuming opik_litellm_monitor is imported and configured elsewhere
|
183
|
+
api_params = opik_litellm_monitor.try_add_opik_monitoring_to_params(
|
184
|
+
api_params
|
185
|
+
)
|
186
|
+
logger.debug("Opik monitoring hooks added to LiteLLM params.")
|
187
|
+
except Exception as e:
|
188
|
+
logger.warning(f"Could not add Opik monitoring to LiteLLM params: {e}")
|
189
|
+
|
190
|
+
logger.debug(
|
191
|
+
f"Final API params (excluding messages): { {k:v for k,v in api_params.items() if k != 'messages'} }"
|
192
|
+
)
|
193
|
+
|
194
|
+
# Increment Counter
|
195
|
+
self.llm_call_counter += 1
|
196
|
+
logger.debug(f"LLM Call Count: {self.llm_call_counter}")
|
197
|
+
|
198
|
+
try:
|
199
|
+
response = litellm.completion(**api_params)
|
200
|
+
model_output = response.choices[0].message.content.strip()
|
201
|
+
logger.debug(f"Model response from {model_to_use}: {model_output[:100]}...")
|
202
|
+
return model_output
|
203
|
+
except litellm.exceptions.RateLimitError as e:
|
204
|
+
logger.error(f"LiteLLM Rate Limit Error for model {model_to_use}: {e}")
|
205
|
+
# Consider adding retry logic here with tenacity
|
206
|
+
raise
|
207
|
+
except litellm.exceptions.APIConnectionError as e:
|
208
|
+
logger.error(f"LiteLLM API Connection Error for model {model_to_use}: {e}")
|
209
|
+
# Consider adding retry logic here
|
210
|
+
raise
|
211
|
+
except litellm.exceptions.ContextWindowExceededError as e:
|
212
|
+
logger.error(
|
213
|
+
f"LiteLLM Context Window Exceeded Error for model {model_to_use}. Prompt length: {len(prompt)}. Details: {e}"
|
214
|
+
)
|
215
|
+
raise
|
216
|
+
except litellm.exceptions.APIError as e: # Catch broader API errors
|
217
|
+
logger.error(f"LiteLLM API Error for model {model_to_use}: {e}")
|
218
|
+
raise
|
219
|
+
except Exception as e:
|
220
|
+
# Catch any other unexpected errors
|
221
|
+
logger.error(
|
222
|
+
f"Unexpected error during model call to {model_to_use}: {type(e).__name__} - {e}"
|
223
|
+
)
|
224
|
+
raise
|
225
|
+
|
226
|
+
def update_optimization(self, optimization, status: str) -> None:
|
227
|
+
"""
|
228
|
+
Update the optimization status
|
229
|
+
"""
|
230
|
+
# FIXME: remove when a solution is added to opik's optimization.update method
|
231
|
+
count = 0
|
232
|
+
while count < 3:
|
233
|
+
try:
|
234
|
+
optimization.update(status="completed")
|
235
|
+
break
|
236
|
+
except ApiError:
|
237
|
+
count += 1
|
238
|
+
time.sleep(5)
|
239
|
+
if count == 3:
|
240
|
+
logger.warning("Unable to update optimization status; continuing...")
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
import litellm
|
4
|
+
from litellm.caching import Cache
|
5
|
+
|
6
|
+
# Configure cache directory
|
7
|
+
CACHE_DIR = os.path.expanduser("~/.litellm_cache")
|
8
|
+
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
9
|
+
|
10
|
+
# Configure cache settings
|
11
|
+
CACHE_CONFIG = {
|
12
|
+
"type": "disk",
|
13
|
+
"disk_cache_dir": CACHE_DIR,
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize_cache():
|
17
|
+
"""Initialize the LiteLLM cache with custom configuration."""
|
18
|
+
litellm.cache = Cache(**CACHE_CONFIG)
|
19
|
+
return litellm.cache
|
20
|
+
|
21
|
+
def clear_cache():
|
22
|
+
"""Clear the LiteLLM cache."""
|
23
|
+
if litellm.cache:
|
24
|
+
litellm.cache.clear()
|
@@ -0,0 +1,112 @@
|
|
1
|
+
from urllib.parse import urlparse, parse_qs
|
2
|
+
import sqlite3
|
3
|
+
import shutil
|
4
|
+
import os
|
5
|
+
import litellm
|
6
|
+
from litellm.caching import Cache
|
7
|
+
import requests
|
8
|
+
|
9
|
+
NAMED_CACHES = {
|
10
|
+
"test": "https://drive.google.com/file/d/1RifNtpN-pl0DW49daRaAMJwW7MCsOh6y/view?usp=sharing",
|
11
|
+
"test2": "https://drive.google.com/uc?id=1RifNtpN-pl0DW49daRaAMJwW7MCsOh6y&export=download",
|
12
|
+
}
|
13
|
+
CACHE_DIR = os.path.expanduser("~/.litellm_cache")
|
14
|
+
|
15
|
+
|
16
|
+
def get_litellm_cache(name: str):
|
17
|
+
"""
|
18
|
+
Get a LiteLLM cache from a remote location, and add it to the
|
19
|
+
local cache
|
20
|
+
"""
|
21
|
+
# Try to close an existing one, if there is one:
|
22
|
+
try:
|
23
|
+
litellm.cache.cache.disk_cache.close()
|
24
|
+
except Exception:
|
25
|
+
pass
|
26
|
+
|
27
|
+
if not os.path.exists(CACHE_DIR):
|
28
|
+
os.makedirs(CACHE_DIR)
|
29
|
+
|
30
|
+
if name.lower() in NAMED_CACHES:
|
31
|
+
return get_litellm_cache(NAMED_CACHES[name.lower()])
|
32
|
+
elif name.startswith("https://drive.google.com/file/d/"):
|
33
|
+
file_id = name.split("/d/")[1].split("/view")[0]
|
34
|
+
download_url = f"https://drive.google.com/uc?id={file_id}&export=download"
|
35
|
+
file_path = _get_google_drive_file(download_url)
|
36
|
+
elif name.startswith("https://drive.google.com/uc"):
|
37
|
+
file_path = _get_google_drive_file(name)
|
38
|
+
else:
|
39
|
+
raise Exception("Unknown cache type: %r" % name)
|
40
|
+
|
41
|
+
dest_path = os.path.join(CACHE_DIR, "cache.db")
|
42
|
+
|
43
|
+
if os.path.exists(dest_path):
|
44
|
+
# Copy contents from source to dest:
|
45
|
+
_copy_cache(file_path, dest_path)
|
46
|
+
else:
|
47
|
+
# Just copy the file:
|
48
|
+
shutil.copy(file_path, dest_path)
|
49
|
+
|
50
|
+
# Update the cache to use the new database:
|
51
|
+
litellm.cache = Cache(type="disk", disk_cache_dir=CACHE_DIR)
|
52
|
+
|
53
|
+
|
54
|
+
def _copy_cache(source_path, dest_path):
|
55
|
+
"""
|
56
|
+
Copy cached items from a source to a destination cache.
|
57
|
+
"""
|
58
|
+
source_conn = sqlite3.connect(source_path)
|
59
|
+
source_conn.row_factory = sqlite3.Row
|
60
|
+
source_cursor = source_conn.cursor()
|
61
|
+
|
62
|
+
dest_conn = sqlite3.connect(dest_path)
|
63
|
+
dest_cursor = dest_conn.cursor()
|
64
|
+
|
65
|
+
source_cursor.execute(f"PRAGMA table_info(Cache)")
|
66
|
+
columns_info = source_cursor.fetchall()
|
67
|
+
column_names = [info[1] for info in columns_info[1:]] # Skip rowid
|
68
|
+
placeholders = ", ".join(["?"] * len(column_names))
|
69
|
+
columns_str = ", ".join(column_names)
|
70
|
+
|
71
|
+
inserted_count = 0
|
72
|
+
source_cursor.execute("SELECT * FROM Cache")
|
73
|
+
records = source_cursor.fetchall()
|
74
|
+
for record in records:
|
75
|
+
record = dict(record)
|
76
|
+
del record["rowid"]
|
77
|
+
key_value = record["key"]
|
78
|
+
|
79
|
+
dest_cursor.execute("SELECT 1 FROM Cache WHERE key = ?", (key_value,))
|
80
|
+
existing_record = dest_cursor.fetchone()
|
81
|
+
|
82
|
+
if not existing_record:
|
83
|
+
dest_cursor.execute(
|
84
|
+
f"INSERT INTO Cache ({columns_str}) VALUES ({placeholders})",
|
85
|
+
list(record.values()),
|
86
|
+
)
|
87
|
+
inserted_count += 1
|
88
|
+
|
89
|
+
print(f"Inserted {inserted_count} record(s) in litellm cache")
|
90
|
+
dest_conn.commit()
|
91
|
+
|
92
|
+
|
93
|
+
def _get_google_drive_file(file_url):
|
94
|
+
"""
|
95
|
+
Given a common google drive URL with id=ID
|
96
|
+
get it, or use cache.
|
97
|
+
"""
|
98
|
+
parsed_url = urlparse(file_url)
|
99
|
+
query_params = parse_qs(parsed_url.query)
|
100
|
+
id_value = query_params.get("id")[0]
|
101
|
+
|
102
|
+
cache_file_path = os.path.join(CACHE_DIR, id_value)
|
103
|
+
|
104
|
+
if not os.path.exists(cache_file_path):
|
105
|
+
response = requests.get(file_url)
|
106
|
+
response.raise_for_status()
|
107
|
+
|
108
|
+
with open(cache_file_path, "wb") as tmp_file:
|
109
|
+
for chunk in response.iter_content(chunk_size=8192):
|
110
|
+
tmp_file.write(chunk)
|
111
|
+
|
112
|
+
return cache_file_path
|