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.

Files changed (53) hide show
  1. {fairo-25.9.4 → fairo-25.9.5}/PKG-INFO +1 -1
  2. fairo-25.9.5/fairo/__init__.py +1 -0
  3. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/agent_serializer.py +172 -13
  4. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/plot.py +23 -2
  5. fairo-25.9.5/fairo/core/utils.py +260 -0
  6. {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/PKG-INFO +1 -1
  7. fairo-25.9.4/fairo/__init__.py +0 -1
  8. fairo-25.9.4/fairo/core/utils.py +0 -54
  9. {fairo-25.9.4 → fairo-25.9.5}/README.md +0 -0
  10. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/__init__.py +0 -0
  11. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/__init__.py +0 -0
  12. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/base_agent.py +0 -0
  13. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/code_analysis_agent.py +0 -0
  14. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/__init__.py +0 -0
  15. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/base_output.py +0 -0
  16. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/output/google_drive.py +0 -0
  17. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/__init__.py +0 -0
  18. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/base_tools.py +0 -0
  19. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/code_analysis.py +0 -0
  20. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/tools/utils.py +0 -0
  21. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/agent/utils.py +0 -0
  22. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/chat/__init__.py +0 -0
  23. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/chat/chat.py +0 -0
  24. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/client/__init__.py +0 -0
  25. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/client/client.py +0 -0
  26. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/exceptions.py +0 -0
  27. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/__init__.py +0 -0
  28. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/env_finder.py +0 -0
  29. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/executor.py +0 -0
  30. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/execution/model_log_helper.py +0 -0
  31. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/__init__.py +0 -0
  32. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/custom_field_value.py +0 -0
  33. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/models/resources.py +0 -0
  34. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/runnable/__init__.py +0 -0
  35. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/runnable/runnable.py +0 -0
  36. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/__init__.py +0 -0
  37. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/tools/suggestion.py +0 -0
  38. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/__init__.py +0 -0
  39. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/base_workflow.py +0 -0
  40. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/dependency.py +0 -0
  41. {fairo-25.9.4 → fairo-25.9.5}/fairo/core/workflow/utils.py +0 -0
  42. {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/__init__.py +0 -0
  43. {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/fairness_object.py +0 -0
  44. {fairo-25.9.4 → fairo-25.9.5}/fairo/metrics/metrics.py +0 -0
  45. {fairo-25.9.4 → fairo-25.9.5}/fairo/settings.py +0 -0
  46. {fairo-25.9.4 → fairo-25.9.5}/fairo/tests/__init__.py +0 -0
  47. {fairo-25.9.4 → fairo-25.9.5}/fairo/tests/test_metrics.py +0 -0
  48. {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/SOURCES.txt +0 -0
  49. {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/dependency_links.txt +0 -0
  50. {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/requires.txt +0 -0
  51. {fairo-25.9.4 → fairo-25.9.5}/fairo.egg-info/top_level.txt +0 -0
  52. {fairo-25.9.4 → fairo-25.9.5}/pyproject.toml +0 -0
  53. {fairo-25.9.4 → fairo-25.9.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fairo
3
- Version: 25.9.4
3
+ Version: 25.9.5
4
4
  Summary: SDK for interfacing with Fairo SaaS platform.
5
5
  Author-email: "Fairo Systems, Inc." <support@fairo.ai>
6
6
  License: Apache-2.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 cloudpickle
4
- import os
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(inputs: Dict[str, Any]) -> Dict[str, Any]:
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
- return agent.invoke(inputs)
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
- runnables.append(agent)
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
- # Return a stringified JSON so the LLM can pass it through easily
221
- return f"""data:{result.mime_type};base64,{result.data_base64}"""
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
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fairo
3
- Version: 25.9.4
3
+ Version: 25.9.5
4
4
  Summary: SDK for interfacing with Fairo SaaS platform.
5
5
  Author-email: "Fairo Systems, Inc." <support@fairo.ai>
6
6
  License: Apache-2.0
@@ -1 +0,0 @@
1
- __version__ = "25.9.4"
@@ -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