fairo 25.9.4__tar.gz → 25.9.5__tar.gz
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.
Potentially problematic release.
This version of fairo might be problematic. Click here for more details.
- {fairo-25.9.4 → fairo-25.9.5}/PKG-INFO +1 -1
- fairo-25.9.5/fairo/__init__.py +1 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/agent_serializer.py +172 -13
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/plot.py +23 -2
- fairo-25.9.5/fairo/core/utils.py +260 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/PKG-INFO +1 -1
- fairo-25.9.4/fairo/__init__.py +0 -1
- fairo-25.9.4/fairo/core/utils.py +0 -54
- {fairo-25.9.4 → fairo-25.9.5}/README.md +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/base_agent.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/code_analysis_agent.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/base_output.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/google_drive.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/base_tools.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/code_analysis.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/utils.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/utils.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/chat/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/chat/chat.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/client/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/client/client.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/exceptions.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/env_finder.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/executor.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/model_log_helper.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/custom_field_value.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/resources.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/runnable/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/runnable/runnable.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/suggestion.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/base_workflow.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/dependency.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/utils.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/fairness_object.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/metrics.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/settings.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/tests/__init__.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo/tests/test_metrics.py +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/SOURCES.txt +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/dependency_links.txt +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/requires.txt +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/top_level.txt +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/pyproject.toml +0 -0
- {fairo-25.9.4 → fairo-25.9.5}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "25.9.5"
|
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
from typing import Any, Dict
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
2
|
import mlflow
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import sys
|
|
3
|
+
from langchain.callbacks.base import BaseCallbackHandler
|
|
4
|
+
from langchain_core.callbacks import CallbackManagerForChainRun
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from langchain_core.runnables import RunnableLambda, Runnable
|
|
8
7
|
from langchain.chains import SimpleSequentialChain
|
|
9
8
|
import logging
|
|
10
9
|
import types
|
|
11
10
|
import threading
|
|
11
|
+
import inspect
|
|
12
12
|
import pandas as pd
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
+
# Thread-local context for S3 client and bucket path to prevent cross-execution contamination
|
|
16
|
+
import threading
|
|
17
|
+
_agent_context = threading.local()
|
|
18
|
+
|
|
19
|
+
def set_agent_context(s3_client, bucket_path, execution_id=None):
|
|
20
|
+
"""Set thread-local context for agent execution"""
|
|
21
|
+
_agent_context.s3_client = s3_client
|
|
22
|
+
_agent_context.bucket_path = bucket_path
|
|
23
|
+
_agent_context.execution_id = execution_id
|
|
24
|
+
|
|
25
|
+
def get_agent_context():
|
|
26
|
+
"""Get thread-local context for agent execution"""
|
|
27
|
+
return {
|
|
28
|
+
's3_client': getattr(_agent_context, 's3_client', None),
|
|
29
|
+
'bucket_path': getattr(_agent_context, 'bucket_path', None),
|
|
30
|
+
'execution_id': getattr(_agent_context, 'execution_id', None)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def clear_agent_context():
|
|
34
|
+
"""Clear thread-local context to prevent memory leaks"""
|
|
35
|
+
for attr in ['s3_client', 'bucket_path', 'execution_id']:
|
|
36
|
+
if hasattr(_agent_context, attr):
|
|
37
|
+
delattr(_agent_context, attr)
|
|
38
|
+
|
|
15
39
|
class CustomPythonModel(mlflow.pyfunc.PythonModel):
|
|
16
40
|
def __init__(self):
|
|
17
41
|
self.agent = None
|
|
@@ -60,20 +84,46 @@ class CustomPythonModel(mlflow.pyfunc.PythonModel):
|
|
|
60
84
|
return self.agent.run(model_input)
|
|
61
85
|
|
|
62
86
|
class AgentChainWrapper:
|
|
63
|
-
def __init__(self, chain_class = SimpleSequentialChain, agent_functions_list = []):
|
|
87
|
+
def __init__(self, chain_class = SimpleSequentialChain, agent_functions_list = [], callback_enabled = False):
|
|
64
88
|
self.chain_class = chain_class
|
|
65
89
|
self.agents = [func() for func in agent_functions_list]
|
|
66
90
|
self.agent_functions = agent_functions_list
|
|
91
|
+
self.callback_enabled = callback_enabled
|
|
67
92
|
|
|
68
93
|
def _wrap_agent_runnable(self, agent) -> RunnableLambda:
|
|
69
94
|
"""
|
|
70
95
|
Wraps the agent's .run() method into a RunnableLambda with a custom function name.
|
|
71
96
|
Properly propagates errors instead of continuing to the next agent.
|
|
72
97
|
"""
|
|
73
|
-
def base_fn(
|
|
98
|
+
def base_fn(
|
|
99
|
+
x: Dict[str, Any],
|
|
100
|
+
*,
|
|
101
|
+
run_manager: CallbackManagerForChainRun = None,
|
|
102
|
+
):
|
|
74
103
|
# Run the agent, but don't catch exceptions - let them propagate
|
|
75
104
|
# This will stop the entire pipeline on agent failure
|
|
76
|
-
|
|
105
|
+
if run_manager:
|
|
106
|
+
run_manager.on_text(f"[{agent.__class__.__name__}] starting…")
|
|
107
|
+
|
|
108
|
+
# If your agent supports .invoke, prefer it; otherwise fall back to .run
|
|
109
|
+
try:
|
|
110
|
+
# Propagate callbacks to the inner agent call too (if it’s a Runnable)
|
|
111
|
+
if hasattr(agent, "invoke"):
|
|
112
|
+
sig = inspect.signature(agent.invoke)
|
|
113
|
+
if "config" in sig.parameters and self.callback_enabled:
|
|
114
|
+
out = agent.invoke(
|
|
115
|
+
x,
|
|
116
|
+
config={"callbacks": [OutputAgentStatus()]}
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
out = agent.invoke(x)
|
|
120
|
+
else:
|
|
121
|
+
out = agent.run(x) # legacy agents
|
|
122
|
+
finally:
|
|
123
|
+
if run_manager:
|
|
124
|
+
run_manager.on_text(f"[{agent.__class__.__name__}] finished.")
|
|
125
|
+
|
|
126
|
+
return out
|
|
77
127
|
|
|
78
128
|
# Check if result starts with "An error occurred" which indicates agent failure
|
|
79
129
|
# if isinstance(result, str) and result.startswith("An error occurred during execution:"):
|
|
@@ -94,7 +144,9 @@ class AgentChainWrapper:
|
|
|
94
144
|
|
|
95
145
|
return RunnableLambda(runnable_fn)
|
|
96
146
|
|
|
97
|
-
def run(self, query):
|
|
147
|
+
def run(self, query, callback_enabled: Optional[bool] = False):
|
|
148
|
+
if callback_enabled:
|
|
149
|
+
self.callback_enabled = callback_enabled
|
|
98
150
|
result = query
|
|
99
151
|
def is_dataframe(obj) -> bool:
|
|
100
152
|
try:
|
|
@@ -106,7 +158,14 @@ class AgentChainWrapper:
|
|
|
106
158
|
runnables = []
|
|
107
159
|
for agent in self.agents:
|
|
108
160
|
if isinstance(agent, Runnable):
|
|
109
|
-
|
|
161
|
+
# Check if agent supports with_config (Runnable style)
|
|
162
|
+
if hasattr(agent, "with_config") and self.callback_enabled:
|
|
163
|
+
# Inject default callbacks on the agent itself
|
|
164
|
+
enhanced = agent.with_config({"callbacks": [OutputAgentStatus()]})
|
|
165
|
+
runnables.append(enhanced)
|
|
166
|
+
else:
|
|
167
|
+
# Not a Runnable — wrap with your fallback wrapper
|
|
168
|
+
runnables.append(agent)
|
|
110
169
|
else:
|
|
111
170
|
runnables.append(
|
|
112
171
|
self._wrap_agent_runnable(agent)
|
|
@@ -123,7 +182,8 @@ class AgentChainWrapper:
|
|
|
123
182
|
)
|
|
124
183
|
return chain.run(result)
|
|
125
184
|
|
|
126
|
-
def predict(self, context = "", model_input = ""):
|
|
185
|
+
def predict(self, context = "", model_input = "", callback_enabled: Optional[bool] = False):
|
|
186
|
+
self.callback_enabled = callback_enabled
|
|
127
187
|
return self.run(model_input)
|
|
128
188
|
|
|
129
189
|
class CustomChainModel(mlflow.pyfunc.PythonModel):
|
|
@@ -191,9 +251,9 @@ class CustomChainModel(mlflow.pyfunc.PythonModel):
|
|
|
191
251
|
agent_functions.append(agent_function)
|
|
192
252
|
|
|
193
253
|
# Create the agent chain
|
|
194
|
-
self.agent_chain = AgentChainWrapper(agent_functions_list=agent_functions)
|
|
254
|
+
self.agent_chain = AgentChainWrapper(agent_functions_list=agent_functions, callback_enabled=True)
|
|
195
255
|
|
|
196
|
-
def predict(self, context, model_input):
|
|
256
|
+
def predict(self, context, model_input, callback_enabled: Optional[bool] = False):
|
|
197
257
|
if isinstance(model_input, list):
|
|
198
258
|
return [self.agent_chain.run(query) for query in model_input]
|
|
199
259
|
else:
|
|
@@ -285,4 +345,103 @@ class CustomCrewModel(mlflow.pyfunc.PythonModel):
|
|
|
285
345
|
if isinstance(model_input, list):
|
|
286
346
|
return [self.agent.run(query) for query in model_input]
|
|
287
347
|
else:
|
|
288
|
-
return self.agent.run(model_input)
|
|
348
|
+
return self.agent.run(model_input)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class OutputAgentStatus(BaseCallbackHandler):
|
|
352
|
+
def __init__(self, s3_client=None, bucket_path=None):
|
|
353
|
+
super().__init__()
|
|
354
|
+
self.s3_client = s3_client
|
|
355
|
+
self.bucket_path = bucket_path
|
|
356
|
+
|
|
357
|
+
# If not provided, try to get from global context
|
|
358
|
+
if not self.s3_client or not self.bucket_path:
|
|
359
|
+
context = get_agent_context()
|
|
360
|
+
self.s3_client = self.s3_client or context.get('s3_client')
|
|
361
|
+
self.bucket_path = self.bucket_path or context.get('bucket_path')
|
|
362
|
+
|
|
363
|
+
def save_to_s3(self, status, message):
|
|
364
|
+
if not self.s3_client or not self.bucket_path:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
# Validate execution_id is in bucket_path to prevent cross-execution contamination
|
|
368
|
+
import os
|
|
369
|
+
execution_id = os.environ.get('EXECUTION_ID')
|
|
370
|
+
if execution_id and execution_id not in self.bucket_path:
|
|
371
|
+
print(f"Warning: Execution ID {execution_id} not found in bucket path {self.bucket_path}. Skipping S3 write.")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
import os
|
|
376
|
+
import json
|
|
377
|
+
from datetime import datetime
|
|
378
|
+
|
|
379
|
+
bucket_name = os.environ.get('DEPLOYMENTS_BUCKET_NAME', 'local-development-deployments')
|
|
380
|
+
status_key = f"{self.bucket_path}/last_output.json"
|
|
381
|
+
|
|
382
|
+
# Try to read existing last_output.json
|
|
383
|
+
existing_data = {}
|
|
384
|
+
try:
|
|
385
|
+
response = self.s3_client.get_object(Bucket=bucket_name, Key=status_key)
|
|
386
|
+
existing_data = json.loads(response['Body'].read().decode('utf-8'))
|
|
387
|
+
except self.s3_client.exceptions.NoSuchKey:
|
|
388
|
+
# File doesn't exist yet, start with empty data
|
|
389
|
+
pass
|
|
390
|
+
except Exception as e:
|
|
391
|
+
print(f"Warning: Could not read existing last_output.json: {e}")
|
|
392
|
+
|
|
393
|
+
# Update the status and output fields
|
|
394
|
+
existing_data.update({
|
|
395
|
+
"status": status,
|
|
396
|
+
"output": message
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
# Save updated last_output.json
|
|
400
|
+
self.s3_client.put_object(
|
|
401
|
+
Bucket=bucket_name,
|
|
402
|
+
Key=status_key,
|
|
403
|
+
Body=json.dumps(existing_data),
|
|
404
|
+
ContentType='application/json'
|
|
405
|
+
)
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f"Error saving status to S3: {e}")
|
|
408
|
+
|
|
409
|
+
def on_text(self, text: str, **kwargs):
|
|
410
|
+
self.save_to_s3("text_output", f"Agent generated text: {text}")
|
|
411
|
+
|
|
412
|
+
def on_llm_start(self, serialized, prompts, **kwargs):
|
|
413
|
+
model_name = serialized.get('name', 'Unknown')
|
|
414
|
+
self.save_to_s3("llm_start", f"Thinking")
|
|
415
|
+
|
|
416
|
+
def on_llm_new_token(self, token: str, **kwargs):
|
|
417
|
+
self.save_to_s3("llm_streaming", f"LLM generating response token: {token}")
|
|
418
|
+
|
|
419
|
+
def on_llm_end(self, response, **kwargs):
|
|
420
|
+
token_count = getattr(response, 'llm_output', {}).get('token_usage', {}).get('total_tokens', 'unknown')
|
|
421
|
+
self.save_to_s3("llm_complete", f"LLM completed response generation (tokens: {token_count})")
|
|
422
|
+
|
|
423
|
+
def on_tool_start(self, serialized, input_str: str, **kwargs):
|
|
424
|
+
tool_name = serialized.get('name', 'Unknown Tool')
|
|
425
|
+
self.save_to_s3("tool_start", f"Executing tool: {tool_name} with input: {input_str[:100]}")
|
|
426
|
+
|
|
427
|
+
def on_tool_end(self, output: str, **kwargs):
|
|
428
|
+
output_preview = str(output)[:100] if len(str(output)) > 100 else str(output)
|
|
429
|
+
self.save_to_s3("tool_complete", f"Tool execution completed with output: {output_preview}")
|
|
430
|
+
|
|
431
|
+
def on_chain_start(self, serialized, inputs, **kwargs):
|
|
432
|
+
chain_id = serialized.get('id', 'Unknown Chain')
|
|
433
|
+
self.save_to_s3("chain_start", f"Starting chain execution: {chain_id}")
|
|
434
|
+
|
|
435
|
+
def on_chain_end(self, outputs, **kwargs):
|
|
436
|
+
output_preview = str(outputs)[:100] if len(str(outputs)) > 100 else str(outputs)
|
|
437
|
+
self.save_to_s3("chain_complete", f"Chain execution completed with outputs: {output_preview}")
|
|
438
|
+
|
|
439
|
+
def on_agent_action(self, action, **kwargs):
|
|
440
|
+
action_tool = getattr(action, 'tool', 'Unknown')
|
|
441
|
+
action_input = getattr(action, 'tool_input', '')
|
|
442
|
+
self.save_to_s3("agent_action", f"Agent taking action with tool: {action_tool}, input: {str(action_input)[:100]}")
|
|
443
|
+
|
|
444
|
+
def on_agent_finish(self, finish, **kwargs):
|
|
445
|
+
return_values = getattr(finish, 'return_values', {})
|
|
446
|
+
output_preview = str(return_values)[:100] if len(str(return_values)) > 100 else str(return_values)
|
|
447
|
+
self.save_to_s3("agent_complete", f"Agent execution finished with result: {output_preview}")
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import uuid
|
|
4
|
+
from langchain_core.messages import ToolMessage
|
|
1
5
|
from typing import Dict, List, Literal, Optional, Union
|
|
2
6
|
from pydantic import BaseModel, Field, ConfigDict
|
|
3
7
|
from enum import Enum
|
|
@@ -197,6 +201,18 @@ class PlotReturn(BaseModel):
|
|
|
197
201
|
alt_text: Optional[str] = None
|
|
198
202
|
debug: Optional[str] = None
|
|
199
203
|
|
|
204
|
+
def save_to_png_file(base64_data: str):
|
|
205
|
+
artifact_id = uuid.uuid4()
|
|
206
|
+
base_dir = Path("/tmp") if Path("/tmp").exists() else Path.cwd()
|
|
207
|
+
file_path = base_dir / f"{artifact_id}.png"
|
|
208
|
+
|
|
209
|
+
if not os.path.exists(file_path):
|
|
210
|
+
# Decode base64 data and write as binary PNG file
|
|
211
|
+
png_data = base64.b64decode(base64_data)
|
|
212
|
+
with open(file_path, "wb") as f:
|
|
213
|
+
f.write(png_data)
|
|
214
|
+
|
|
215
|
+
return artifact_id
|
|
200
216
|
|
|
201
217
|
@tool(args_schema=PlotSpec)
|
|
202
218
|
def generate_plot(**kwargs) -> str:
|
|
@@ -217,8 +233,13 @@ def generate_plot(**kwargs) -> str:
|
|
|
217
233
|
f"{spec.chart_type} plot" if spec.chart_type else "Plot"
|
|
218
234
|
)
|
|
219
235
|
result = PlotReturn(data_base64=b64, alt_text=alt)
|
|
220
|
-
#
|
|
221
|
-
|
|
236
|
+
# Save the base64 data as a PNG file
|
|
237
|
+
artifact_id = save_to_png_file(result.data_base64)
|
|
238
|
+
return ToolMessage(
|
|
239
|
+
content=f"Plot successfully generated",
|
|
240
|
+
artifact={"artifact_id": artifact_id, "artifact_type": "png"},
|
|
241
|
+
tool_call_id=uuid.uuid4()
|
|
242
|
+
)
|
|
222
243
|
except Exception as e:
|
|
223
244
|
# Return an error payload the orchestrator can handle
|
|
224
245
|
err = PlotReturn(
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import mimetypes
|
|
4
|
+
from typing import Optional, Dict, Any, Tuple
|
|
5
|
+
from langchain_core.messages import ToolMessage
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from fairo.settings import get_fairo_base_url, get_fairo_api_key, get_fairo_api_secret
|
|
8
|
+
from fairo.core.client.client import BaseClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_fairo_client(base_url: Optional[str] = None) -> BaseClient:
|
|
12
|
+
"""
|
|
13
|
+
Setup Fairo BaseClient with authentication.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
base_url: Optional base URL, defaults to get_fairo_base_url()
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Configured BaseClient instance
|
|
20
|
+
"""
|
|
21
|
+
api_base_url = base_url or get_fairo_base_url()
|
|
22
|
+
auth_token = os.environ.get("FAIRO_AUTH_TOKEN")
|
|
23
|
+
api_key = get_fairo_api_key()
|
|
24
|
+
api_secret = get_fairo_api_secret()
|
|
25
|
+
|
|
26
|
+
# Initialize client following the same pattern as FairoVectorStore
|
|
27
|
+
client = BaseClient(
|
|
28
|
+
base_url=api_base_url.rstrip('/'),
|
|
29
|
+
username=api_key,
|
|
30
|
+
password=api_secret,
|
|
31
|
+
fairo_auth_token=auth_token
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return client
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_file_mimetype(file_path: Path) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Get the MIME type for a file based on its extension.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
file_path: Path to the file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
MIME type string
|
|
46
|
+
"""
|
|
47
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
48
|
+
return mime_type or 'application/octet-stream'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def upload_file_to_fairo(
|
|
52
|
+
file_path: Path,
|
|
53
|
+
deployment_id: Optional[str] = None,
|
|
54
|
+
execution_id: Optional[str] = None,
|
|
55
|
+
base_url: Optional[str] = None
|
|
56
|
+
) -> Optional[Tuple[str, str]]:
|
|
57
|
+
"""
|
|
58
|
+
Upload a file to Fairo API using BaseClient authentication pattern.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: Path to the file to upload
|
|
62
|
+
deployment_id: Optional deployment ID for deployment_artifacts endpoint
|
|
63
|
+
execution_id: Optional execution ID for deployment_artifacts endpoint
|
|
64
|
+
base_url: Optional base URL, defaults to get_fairo_base_url()
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (file_id, file_url) if upload successful, None otherwise
|
|
68
|
+
"""
|
|
69
|
+
if not file_path.exists():
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Setup client for authentication
|
|
74
|
+
api_base_url = base_url or get_fairo_base_url()
|
|
75
|
+
auth_token = os.environ.get("FAIRO_AUTH_TOKEN")
|
|
76
|
+
api_key = get_fairo_api_key()
|
|
77
|
+
api_secret = get_fairo_api_secret()
|
|
78
|
+
|
|
79
|
+
# Setup session with authentication (same pattern as BaseClient)
|
|
80
|
+
session = requests.Session()
|
|
81
|
+
if auth_token:
|
|
82
|
+
session.headers.update({"Authorization": f"Bearer {auth_token}"})
|
|
83
|
+
elif api_key and api_secret:
|
|
84
|
+
session.auth = requests.auth.HTTPBasicAuth(api_key, api_secret)
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError("Must provide either FAIRO_AUTH_TOKEN or API credentials")
|
|
87
|
+
|
|
88
|
+
# Determine endpoint and data based on available IDs
|
|
89
|
+
if deployment_id and execution_id:
|
|
90
|
+
endpoint = f"{api_base_url.rstrip('/')}/deployment_artifacts"
|
|
91
|
+
upload_data = {
|
|
92
|
+
'deployment': deployment_id,
|
|
93
|
+
'execution_id': execution_id
|
|
94
|
+
}
|
|
95
|
+
else:
|
|
96
|
+
endpoint = f"{api_base_url.rstrip('/')}/files"
|
|
97
|
+
upload_data = {}
|
|
98
|
+
|
|
99
|
+
# Get file info
|
|
100
|
+
filename = file_path.name
|
|
101
|
+
mime_type = get_file_mimetype(file_path)
|
|
102
|
+
|
|
103
|
+
# Upload file
|
|
104
|
+
with open(file_path, 'rb') as file:
|
|
105
|
+
files = {'file_object': (filename, file, mime_type)}
|
|
106
|
+
response = session.post(
|
|
107
|
+
endpoint,
|
|
108
|
+
data=upload_data,
|
|
109
|
+
files=files
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
|
|
113
|
+
# Get the uploaded file information from response
|
|
114
|
+
upload_result = response.json()
|
|
115
|
+
file_id = upload_result.get("id") or upload_result.get("file_id")
|
|
116
|
+
file_url = upload_result.get("file_relative_url")
|
|
117
|
+
|
|
118
|
+
if file_id and file_url:
|
|
119
|
+
return file_id, f"{api_base_url.rstrip('/')}{file_url}"
|
|
120
|
+
else:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
except requests.exceptions.RequestException as e:
|
|
124
|
+
print(f"Failed to upload file {file_path}: {e}")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def process_artifact_file(
|
|
129
|
+
artifact_id: str,
|
|
130
|
+
file_extension: str = "html",
|
|
131
|
+
deployment_id: Optional[str] = None,
|
|
132
|
+
execution_id: Optional[str] = None
|
|
133
|
+
) -> Optional[Dict[str, Any]]:
|
|
134
|
+
"""
|
|
135
|
+
Process an artifact file by uploading it to Fairo API or returning file data.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
artifact_id: ID of the artifact
|
|
139
|
+
file_extension: File extension (defaults to 'html')
|
|
140
|
+
deployment_id: Optional deployment ID
|
|
141
|
+
execution_id: Optional execution ID
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Message content dict or None if processing fails
|
|
145
|
+
"""
|
|
146
|
+
# Setup file path
|
|
147
|
+
base_dir = Path("/tmp") if Path("/tmp").exists() else Path.cwd()
|
|
148
|
+
artifact_path = base_dir / f"{artifact_id}.{file_extension}"
|
|
149
|
+
|
|
150
|
+
if not artifact_path.exists():
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Try to upload file
|
|
155
|
+
upload_result = upload_file_to_fairo(
|
|
156
|
+
artifact_path,
|
|
157
|
+
deployment_id=deployment_id,
|
|
158
|
+
execution_id=execution_id
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if upload_result:
|
|
162
|
+
file_id, file_url = upload_result
|
|
163
|
+
# Get mime type before cleaning up
|
|
164
|
+
mime_type = get_file_mimetype(artifact_path)
|
|
165
|
+
|
|
166
|
+
# Clean up local file after successful upload
|
|
167
|
+
artifact_path.unlink()
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"type": "file",
|
|
171
|
+
"mimeType": mime_type,
|
|
172
|
+
"url": file_url,
|
|
173
|
+
"id": file_id
|
|
174
|
+
}
|
|
175
|
+
else:
|
|
176
|
+
# Fallback to returning file data
|
|
177
|
+
mime_type = get_file_mimetype(artifact_path)
|
|
178
|
+
file_data = artifact_path.read_text(encoding="utf-8") if mime_type.startswith("text/") else artifact_path.read_bytes()
|
|
179
|
+
|
|
180
|
+
# Clean up local file
|
|
181
|
+
artifact_path.unlink()
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"type": "file",
|
|
185
|
+
"mimeType": mime_type,
|
|
186
|
+
"data": file_data
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"Error processing artifact {artifact_id}: {e}")
|
|
191
|
+
|
|
192
|
+
# Clean up local file in case of error
|
|
193
|
+
if artifact_path.exists():
|
|
194
|
+
artifact_path.unlink()
|
|
195
|
+
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_chat_interface_output(agent_executor_result):
|
|
200
|
+
"""
|
|
201
|
+
Parses agent executor result into chat interface response
|
|
202
|
+
return_intermediate_steps must be set as true on the AgentExecutor in order to properly parse plot and suggestions
|
|
203
|
+
"""
|
|
204
|
+
messages = [{"role": "assistant", "content": [
|
|
205
|
+
{
|
|
206
|
+
"type": "text",
|
|
207
|
+
"text": agent_executor_result["output"]
|
|
208
|
+
}
|
|
209
|
+
]}]
|
|
210
|
+
suggestions = []
|
|
211
|
+
intermediate_steps = agent_executor_result.get('intermediate_steps', [])
|
|
212
|
+
for step, output in intermediate_steps:
|
|
213
|
+
if step.tool == "send_chat_suggestions":
|
|
214
|
+
suggestions = output
|
|
215
|
+
|
|
216
|
+
# Check if some tool message has artifact and raw_html attribute
|
|
217
|
+
artifact = None
|
|
218
|
+
is_tool_msg = isinstance(output, ToolMessage)
|
|
219
|
+
if is_tool_msg:
|
|
220
|
+
artifact = getattr(output, "artifact", None)
|
|
221
|
+
if artifact is None:
|
|
222
|
+
artifact = getattr(output, "additional_kwargs", {}).get("artifact")
|
|
223
|
+
if artifact:
|
|
224
|
+
artifact_id = artifact.get("artifact_id")
|
|
225
|
+
artifact_type = artifact.get("artifact_type", "html") # Default to html for backward compatibility
|
|
226
|
+
|
|
227
|
+
if artifact_id:
|
|
228
|
+
# Get environment variables
|
|
229
|
+
deployment_id = os.environ.get("DEPLOYMENT_ID")
|
|
230
|
+
execution_id = os.environ.get("EXECUTION_ID")
|
|
231
|
+
|
|
232
|
+
# Process artifact using the new modular function
|
|
233
|
+
content = process_artifact_file(
|
|
234
|
+
artifact_id=artifact_id,
|
|
235
|
+
file_extension=artifact_type,
|
|
236
|
+
deployment_id=deployment_id,
|
|
237
|
+
execution_id=execution_id
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if content:
|
|
241
|
+
# Create message with file ID in data field
|
|
242
|
+
if "id" in content:
|
|
243
|
+
messages.append({
|
|
244
|
+
"role": "assistant",
|
|
245
|
+
"content": [{
|
|
246
|
+
"type": "file",
|
|
247
|
+
"data": content["id"],
|
|
248
|
+
"mimeType": content["mimeType"]
|
|
249
|
+
}]
|
|
250
|
+
})
|
|
251
|
+
else:
|
|
252
|
+
# Fallback to original content structure
|
|
253
|
+
messages.append({
|
|
254
|
+
"role": "assistant",
|
|
255
|
+
"content": [content]
|
|
256
|
+
})
|
|
257
|
+
return {
|
|
258
|
+
"messages": messages,
|
|
259
|
+
"suggestions": suggestions
|
|
260
|
+
}
|
fairo-25.9.4/fairo/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "25.9.4"
|
fairo-25.9.4/fairo/core/utils.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from langchain_core.messages import ToolMessage
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
def parse_chat_interface_output(agent_executor_result):
|
|
6
|
-
"""
|
|
7
|
-
Parses agent executor result into chat interface response
|
|
8
|
-
return_intermediate_steps must be set as true on the AgentExecutor in order to properly parse plot and suggestions
|
|
9
|
-
"""
|
|
10
|
-
messages = [{"role": "assistant", "content": [
|
|
11
|
-
{
|
|
12
|
-
"type": "text",
|
|
13
|
-
"text": agent_executor_result["output"]
|
|
14
|
-
}
|
|
15
|
-
]}]
|
|
16
|
-
suggestions = []
|
|
17
|
-
intermediate_steps = agent_executor_result.get('intermediate_steps', [])
|
|
18
|
-
for step, output in intermediate_steps:
|
|
19
|
-
if step.tool == "generate_plot":
|
|
20
|
-
messages.append({"role": "assistant", "content": [
|
|
21
|
-
{
|
|
22
|
-
"type": "image",
|
|
23
|
-
"image": output
|
|
24
|
-
}
|
|
25
|
-
]})
|
|
26
|
-
if step.tool == "send_chat_suggestions":
|
|
27
|
-
suggestions = output
|
|
28
|
-
|
|
29
|
-
# Check if some tool message has artifact and raw_html attribute
|
|
30
|
-
artifact = None
|
|
31
|
-
is_tool_msg = isinstance(output, ToolMessage)
|
|
32
|
-
if is_tool_msg:
|
|
33
|
-
artifact = getattr(output, "artifact", None)
|
|
34
|
-
if artifact is None:
|
|
35
|
-
artifact = getattr(output, "additional_kwargs", {}).get("artifact")
|
|
36
|
-
if artifact:
|
|
37
|
-
artifact_id = artifact.get("artifact_id")
|
|
38
|
-
if artifact_id:
|
|
39
|
-
base_dir = Path("/tmp") if Path("/tmp").exists() else Path.cwd()
|
|
40
|
-
artifact_path = base_dir / f"{artifact_id}.html"
|
|
41
|
-
messages.append({
|
|
42
|
-
"role": "assistant",
|
|
43
|
-
"content": [{
|
|
44
|
-
"type": "file",
|
|
45
|
-
"mimeType": "text/html",
|
|
46
|
-
"data": artifact_path.read_text(encoding="utf-8")
|
|
47
|
-
}]
|
|
48
|
-
})
|
|
49
|
-
if os.path.exists(artifact_path):
|
|
50
|
-
os.remove(artifact_path)
|
|
51
|
-
return {
|
|
52
|
-
"messages": messages,
|
|
53
|
-
"suggestions": suggestions
|
|
54
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|