lfx-nightly 0.1.13.dev0__py3-none-any.whl → 0.2.0.dev0__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.
Files changed (86) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +109 -29
  3. lfx/base/agents/events.py +102 -35
  4. lfx/base/agents/utils.py +15 -2
  5. lfx/base/composio/composio_base.py +24 -9
  6. lfx/base/datastax/__init__.py +5 -0
  7. lfx/{components/vectorstores/astradb.py → base/datastax/astradb_base.py} +84 -473
  8. lfx/base/io/chat.py +5 -4
  9. lfx/base/mcp/util.py +101 -15
  10. lfx/base/models/model_input_constants.py +74 -7
  11. lfx/base/models/ollama_constants.py +3 -0
  12. lfx/base/models/watsonx_constants.py +12 -0
  13. lfx/cli/commands.py +1 -1
  14. lfx/components/agents/__init__.py +3 -1
  15. lfx/components/agents/agent.py +47 -4
  16. lfx/components/agents/altk_agent.py +366 -0
  17. lfx/components/agents/cuga_agent.py +1 -1
  18. lfx/components/agents/mcp_component.py +32 -2
  19. lfx/components/amazon/amazon_bedrock_converse.py +1 -1
  20. lfx/components/apify/apify_actor.py +3 -3
  21. lfx/components/datastax/__init__.py +12 -6
  22. lfx/components/datastax/{astra_assistant_manager.py → astradb_assistant_manager.py} +1 -0
  23. lfx/components/datastax/astradb_chatmemory.py +40 -0
  24. lfx/components/datastax/astradb_cql.py +5 -31
  25. lfx/components/datastax/astradb_graph.py +9 -123
  26. lfx/components/datastax/astradb_tool.py +12 -52
  27. lfx/components/datastax/astradb_vectorstore.py +133 -976
  28. lfx/components/datastax/create_assistant.py +1 -0
  29. lfx/components/datastax/create_thread.py +1 -0
  30. lfx/components/datastax/dotenv.py +1 -0
  31. lfx/components/datastax/get_assistant.py +1 -0
  32. lfx/components/datastax/getenvvar.py +1 -0
  33. lfx/components/datastax/graph_rag.py +1 -1
  34. lfx/components/datastax/list_assistants.py +1 -0
  35. lfx/components/datastax/run.py +1 -0
  36. lfx/components/docling/__init__.py +3 -0
  37. lfx/components/docling/docling_remote_vlm.py +284 -0
  38. lfx/components/ibm/watsonx.py +25 -21
  39. lfx/components/input_output/chat.py +8 -0
  40. lfx/components/input_output/chat_output.py +8 -0
  41. lfx/components/knowledge_bases/ingestion.py +17 -9
  42. lfx/components/knowledge_bases/retrieval.py +16 -8
  43. lfx/components/logic/loop.py +4 -0
  44. lfx/components/mistral/mistral_embeddings.py +1 -1
  45. lfx/components/models/embedding_model.py +88 -7
  46. lfx/components/ollama/ollama.py +221 -14
  47. lfx/components/openrouter/openrouter.py +49 -147
  48. lfx/components/processing/parser.py +6 -1
  49. lfx/components/processing/structured_output.py +55 -17
  50. lfx/components/vectorstores/__init__.py +0 -6
  51. lfx/custom/custom_component/component.py +3 -2
  52. lfx/field_typing/constants.py +1 -0
  53. lfx/graph/edge/base.py +2 -2
  54. lfx/graph/graph/base.py +1 -1
  55. lfx/graph/graph/schema.py +3 -2
  56. lfx/graph/vertex/vertex_types.py +1 -1
  57. lfx/io/schema.py +6 -0
  58. lfx/schema/schema.py +5 -0
  59. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/METADATA +1 -1
  60. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/RECORD +63 -81
  61. lfx/components/datastax/astra_db.py +0 -77
  62. lfx/components/datastax/cassandra.py +0 -92
  63. lfx/components/vectorstores/astradb_graph.py +0 -326
  64. lfx/components/vectorstores/cassandra.py +0 -264
  65. lfx/components/vectorstores/cassandra_graph.py +0 -238
  66. lfx/components/vectorstores/chroma.py +0 -167
  67. lfx/components/vectorstores/clickhouse.py +0 -135
  68. lfx/components/vectorstores/couchbase.py +0 -102
  69. lfx/components/vectorstores/elasticsearch.py +0 -267
  70. lfx/components/vectorstores/faiss.py +0 -111
  71. lfx/components/vectorstores/graph_rag.py +0 -141
  72. lfx/components/vectorstores/hcd.py +0 -314
  73. lfx/components/vectorstores/milvus.py +0 -115
  74. lfx/components/vectorstores/mongodb_atlas.py +0 -213
  75. lfx/components/vectorstores/opensearch.py +0 -243
  76. lfx/components/vectorstores/pgvector.py +0 -72
  77. lfx/components/vectorstores/pinecone.py +0 -134
  78. lfx/components/vectorstores/qdrant.py +0 -109
  79. lfx/components/vectorstores/supabase.py +0 -76
  80. lfx/components/vectorstores/upstash.py +0 -124
  81. lfx/components/vectorstores/vectara.py +0 -97
  82. lfx/components/vectorstores/vectara_rag.py +0 -164
  83. lfx/components/vectorstores/weaviate.py +0 -89
  84. /lfx/components/datastax/{astra_vectorize.py → astradb_vectorize.py} +0 -0
  85. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/WHEEL +0 -0
  86. {lfx_nightly-0.1.13.dev0.dist-info → lfx_nightly-0.2.0.dev0.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,9 @@ from typing import Any
3
3
  from langchain_openai import OpenAIEmbeddings
4
4
 
5
5
  from lfx.base.embeddings.model import LCEmbeddingsModel
6
+ from lfx.base.models.ollama_constants import OLLAMA_EMBEDDING_MODELS
6
7
  from lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES
8
+ from lfx.base.models.watsonx_constants import WATSONX_EMBEDDING_MODEL_NAMES
7
9
  from lfx.field_typing import Embeddings
8
10
  from lfx.io import (
9
11
  BoolInput,
@@ -29,11 +31,11 @@ class EmbeddingModelComponent(LCEmbeddingsModel):
29
31
  DropdownInput(
30
32
  name="provider",
31
33
  display_name="Model Provider",
32
- options=["OpenAI"],
34
+ options=["OpenAI", "Ollama", "IBM watsonx.ai"],
33
35
  value="OpenAI",
34
36
  info="Select the embedding model provider",
35
37
  real_time_refresh=True,
36
- options_metadata=[{"icon": "OpenAI"}],
38
+ options_metadata=[{"icon": "OpenAI"}, {"icon": "Ollama"}, {"icon": "WatsonxAI"}],
37
39
  ),
38
40
  DropdownInput(
39
41
  name="model",
@@ -56,6 +58,13 @@ class EmbeddingModelComponent(LCEmbeddingsModel):
56
58
  info="Base URL for the API. Leave empty for default.",
57
59
  advanced=True,
58
60
  ),
61
+ # Watson-specific inputs
62
+ MessageTextInput(
63
+ name="project_id",
64
+ display_name="Project ID",
65
+ info="IBM watsonx.ai Project ID (required for IBM watsonx.ai)",
66
+ show=False,
67
+ ),
59
68
  IntInput(
60
69
  name="dimensions",
61
70
  display_name="Dimensions",
@@ -102,13 +111,85 @@ class EmbeddingModelComponent(LCEmbeddingsModel):
102
111
  show_progress_bar=show_progress_bar,
103
112
  model_kwargs=model_kwargs,
104
113
  )
114
+
115
+ if provider == "Ollama":
116
+ try:
117
+ from langchain_ollama import OllamaEmbeddings
118
+ except ImportError:
119
+ try:
120
+ from langchain_community.embeddings import OllamaEmbeddings
121
+ except ImportError:
122
+ msg = "Please install langchain-ollama: pip install langchain-ollama"
123
+ raise ImportError(msg) from None
124
+
125
+ return OllamaEmbeddings(
126
+ model=model,
127
+ base_url=api_base or "http://localhost:11434",
128
+ **model_kwargs,
129
+ )
130
+
131
+ if provider == "IBM watsonx.ai":
132
+ try:
133
+ from langchain_ibm import WatsonxEmbeddings
134
+ except ImportError:
135
+ msg = "Please install langchain-ibm: pip install langchain-ibm"
136
+ raise ImportError(msg) from None
137
+
138
+ if not api_key:
139
+ msg = "IBM watsonx.ai API key is required when using IBM watsonx.ai provider"
140
+ raise ValueError(msg)
141
+
142
+ project_id = self.project_id
143
+
144
+ if not project_id:
145
+ msg = "Project ID is required for IBM watsonx.ai provider"
146
+ raise ValueError(msg)
147
+
148
+ params = {
149
+ "model_id": model,
150
+ "url": api_base or "https://us-south.ml.cloud.ibm.com",
151
+ "apikey": api_key,
152
+ }
153
+
154
+ params["project_id"] = project_id
155
+
156
+ return WatsonxEmbeddings(**params)
157
+
105
158
  msg = f"Unknown provider: {provider}"
106
159
  raise ValueError(msg)
107
160
 
108
161
  def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:
109
- if field_name == "provider" and field_value == "OpenAI":
110
- build_config["model"]["options"] = OPENAI_EMBEDDING_MODEL_NAMES
111
- build_config["model"]["value"] = OPENAI_EMBEDDING_MODEL_NAMES[0]
112
- build_config["api_key"]["display_name"] = "OpenAI API Key"
113
- build_config["api_base"]["display_name"] = "OpenAI API Base URL"
162
+ if field_name == "provider":
163
+ if field_value == "OpenAI":
164
+ build_config["model"]["options"] = OPENAI_EMBEDDING_MODEL_NAMES
165
+ build_config["model"]["value"] = OPENAI_EMBEDDING_MODEL_NAMES[0]
166
+ build_config["api_key"]["display_name"] = "OpenAI API Key"
167
+ build_config["api_key"]["required"] = True
168
+ build_config["api_key"]["show"] = True
169
+ build_config["api_base"]["display_name"] = "OpenAI API Base URL"
170
+ build_config["api_base"]["advanced"] = True
171
+ build_config["project_id"]["show"] = False
172
+
173
+ elif field_value == "Ollama":
174
+ build_config["model"]["options"] = OLLAMA_EMBEDDING_MODELS
175
+ build_config["model"]["value"] = OLLAMA_EMBEDDING_MODELS[0]
176
+ build_config["api_key"]["display_name"] = "API Key (Optional)"
177
+ build_config["api_key"]["required"] = False
178
+ build_config["api_key"]["show"] = False
179
+ build_config["api_base"]["display_name"] = "Ollama Base URL"
180
+ build_config["api_base"]["value"] = "http://localhost:11434"
181
+ build_config["api_base"]["advanced"] = True
182
+ build_config["project_id"]["show"] = False
183
+
184
+ elif field_value == "IBM watsonx.ai":
185
+ build_config["model"]["options"] = WATSONX_EMBEDDING_MODEL_NAMES
186
+ build_config["model"]["value"] = WATSONX_EMBEDDING_MODEL_NAMES[0]
187
+ build_config["api_key"]["display_name"] = "IBM watsonx.ai API Key"
188
+ build_config["api_key"]["required"] = True
189
+ build_config["api_key"]["show"] = True
190
+ build_config["api_base"]["display_name"] = "IBM watsonx.ai URL"
191
+ build_config["api_base"]["value"] = "https://us-south.ml.cloud.ibm.com"
192
+ build_config["api_base"]["advanced"] = False
193
+ build_config["project_id"]["show"] = True
194
+
114
195
  return build_config
@@ -1,4 +1,6 @@
1
1
  import asyncio
2
+ import json
3
+ from contextlib import suppress
2
4
  from typing import Any
3
5
  from urllib.parse import urljoin
4
6
 
@@ -8,11 +10,27 @@ from langchain_ollama import ChatOllama
8
10
  from lfx.base.models.model import LCModelComponent
9
11
  from lfx.field_typing import LanguageModel
10
12
  from lfx.field_typing.range_spec import RangeSpec
11
- from lfx.io import BoolInput, DictInput, DropdownInput, FloatInput, IntInput, MessageTextInput, SliderInput
13
+ from lfx.helpers.base_model import build_model_from_schema
14
+ from lfx.io import (
15
+ BoolInput,
16
+ DictInput,
17
+ DropdownInput,
18
+ FloatInput,
19
+ IntInput,
20
+ MessageTextInput,
21
+ Output,
22
+ SecretStrInput,
23
+ SliderInput,
24
+ TableInput,
25
+ )
12
26
  from lfx.log.logger import logger
27
+ from lfx.schema.data import Data
28
+ from lfx.schema.dataframe import DataFrame
29
+ from lfx.schema.table import EditMode
13
30
  from lfx.utils.util import transform_localhost_url
14
31
 
15
32
  HTTP_STATUS_OK = 200
33
+ TABLE_ROW_PLACEHOLDER = {"name": "field", "description": "description of field", "type": "str", "multiple": "False"}
16
34
 
17
35
 
18
36
  class ChatOllamaComponent(LCModelComponent):
@@ -28,11 +46,51 @@ class ChatOllamaComponent(LCModelComponent):
28
46
  DESIRED_CAPABILITY = "completion"
29
47
  TOOL_CALLING_CAPABILITY = "tools"
30
48
 
49
+ # Define the table schema for the format input
50
+ TABLE_SCHEMA = [
51
+ {
52
+ "name": "name",
53
+ "display_name": "Name",
54
+ "type": "str",
55
+ "description": "Specify the name of the output field.",
56
+ "default": "field",
57
+ "edit_mode": EditMode.INLINE,
58
+ },
59
+ {
60
+ "name": "description",
61
+ "display_name": "Description",
62
+ "type": "str",
63
+ "description": "Describe the purpose of the output field.",
64
+ "default": "description of field",
65
+ "edit_mode": EditMode.POPOVER,
66
+ },
67
+ {
68
+ "name": "type",
69
+ "display_name": "Type",
70
+ "type": "str",
71
+ "edit_mode": EditMode.INLINE,
72
+ "description": ("Indicate the data type of the output field (e.g., str, int, float, bool, dict)."),
73
+ "options": ["str", "int", "float", "bool", "dict"],
74
+ "default": "str",
75
+ },
76
+ {
77
+ "name": "multiple",
78
+ "display_name": "As List",
79
+ "type": "boolean",
80
+ "description": "Set to True if this output field should be a list of the specified type.",
81
+ "edit_mode": EditMode.INLINE,
82
+ "options": ["True", "False"],
83
+ "default": "False",
84
+ },
85
+ ]
86
+ default_table_row = {row["name"]: row.get("default", None) for row in TABLE_SCHEMA}
87
+ default_table_row_schema = build_model_from_schema([default_table_row]).model_json_schema()
88
+
31
89
  inputs = [
32
90
  MessageTextInput(
33
91
  name="base_url",
34
- display_name="Base URL",
35
- info="Endpoint of the Ollama API. Defaults to http://localhost:11434 .",
92
+ display_name="Ollama API URL",
93
+ info="Endpoint of the Ollama API. Defaults to http://localhost:11434.",
36
94
  value="http://localhost:11434",
37
95
  real_time_refresh=True,
38
96
  ),
@@ -44,6 +102,15 @@ class ChatOllamaComponent(LCModelComponent):
44
102
  refresh_button=True,
45
103
  real_time_refresh=True,
46
104
  ),
105
+ SecretStrInput(
106
+ name="api_key",
107
+ display_name="Ollama API Key",
108
+ info="Your Ollama API key.",
109
+ value=None,
110
+ required=False,
111
+ real_time_refresh=True,
112
+ advanced=True,
113
+ ),
47
114
  SliderInput(
48
115
  name="temperature",
49
116
  display_name="Temperature",
@@ -51,8 +118,13 @@ class ChatOllamaComponent(LCModelComponent):
51
118
  range_spec=RangeSpec(min=0, max=1, step=0.01),
52
119
  advanced=True,
53
120
  ),
54
- MessageTextInput(
55
- name="format", display_name="Format", info="Specify the format of the output (e.g., json).", advanced=True
121
+ TableInput(
122
+ name="format",
123
+ display_name="Format",
124
+ info="Specify the format of the output.",
125
+ advanced=False,
126
+ table_schema=TABLE_SCHEMA,
127
+ value=default_table_row,
56
128
  ),
57
129
  DictInput(name="metadata", display_name="Metadata", info="Metadata to add to the run trace.", advanced=True),
58
130
  DropdownInput(
@@ -112,7 +184,12 @@ class ChatOllamaComponent(LCModelComponent):
112
184
  name="top_k", display_name="Top K", info="Limits token selection to top K. (Default: 40)", advanced=True
113
185
  ),
114
186
  FloatInput(name="top_p", display_name="Top P", info="Works together with top-k. (Default: 0.9)", advanced=True),
115
- BoolInput(name="verbose", display_name="Verbose", info="Whether to print out response text.", advanced=True),
187
+ BoolInput(
188
+ name="enable_verbose_output",
189
+ display_name="Ollama Verbose Output",
190
+ info="Whether to print out response text.",
191
+ advanced=True,
192
+ ),
116
193
  MessageTextInput(
117
194
  name="tags",
118
195
  display_name="Tags",
@@ -141,15 +218,22 @@ class ChatOllamaComponent(LCModelComponent):
141
218
  *LCModelComponent.get_base_inputs(),
142
219
  ]
143
220
 
221
+ outputs = [
222
+ Output(display_name="Text", name="text_output", method="text_response"),
223
+ Output(display_name="Language Model", name="model_output", method="build_model"),
224
+ Output(display_name="Data", name="data_output", method="build_data_output"),
225
+ Output(display_name="DataFrame", name="dataframe_output", method="build_dataframe_output"),
226
+ ]
227
+
144
228
  def build_model(self) -> LanguageModel: # type: ignore[type-var]
145
229
  # Mapping mirostat settings to their corresponding values
146
230
  mirostat_options = {"Mirostat": 1, "Mirostat 2.0": 2}
147
231
 
148
- # Default to 0 for 'Disabled'
149
- mirostat_value = mirostat_options.get(self.mirostat, 0)
232
+ # Default to None for 'Disabled'
233
+ mirostat_value = mirostat_options.get(self.mirostat, None)
150
234
 
151
235
  # Set mirostat_eta and mirostat_tau to None if mirostat is disabled
152
- if mirostat_value == 0:
236
+ if mirostat_value is None:
153
237
  mirostat_eta = None
154
238
  mirostat_tau = None
155
239
  else:
@@ -169,12 +253,18 @@ class ChatOllamaComponent(LCModelComponent):
169
253
  "Learn more at https://docs.ollama.com/openai#openai-compatibility"
170
254
  )
171
255
 
256
+ try:
257
+ output_format = self._parse_format_field(self.format)
258
+ except Exception as e:
259
+ msg = f"Failed to parse the format field: {e}"
260
+ raise ValueError(msg) from e
261
+
172
262
  # Mapping system settings to their corresponding values
173
263
  llm_params = {
174
264
  "base_url": transformed_base_url,
175
265
  "model": self.model_name,
176
266
  "mirostat": mirostat_value,
177
- "format": self.format,
267
+ "format": output_format,
178
268
  "metadata": self.metadata,
179
269
  "tags": self.tags.split(",") if self.tags else None,
180
270
  "mirostat_eta": mirostat_eta,
@@ -191,9 +281,12 @@ class ChatOllamaComponent(LCModelComponent):
191
281
  "timeout": self.timeout or None,
192
282
  "top_k": self.top_k or None,
193
283
  "top_p": self.top_p or None,
194
- "verbose": self.verbose,
284
+ "verbose": self.enable_verbose_output or False,
195
285
  "template": self.template,
196
286
  }
287
+ headers = self.headers
288
+ if headers is not None:
289
+ llm_params["client_kwargs"] = {"headers": headers}
197
290
 
198
291
  # Remove parameters with None values
199
292
  llm_params = {k: v for k, v in llm_params.items() if v is not None}
@@ -219,7 +312,9 @@ class ChatOllamaComponent(LCModelComponent):
219
312
  url = url.rstrip("/").removesuffix("/v1")
220
313
  if not url.endswith("/"):
221
314
  url = url + "/"
222
- return (await client.get(urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
315
+ return (
316
+ await client.get(url=urljoin(url, "api/tags"), headers=self.headers)
317
+ ).status_code == HTTP_STATUS_OK
223
318
  except httpx.RequestError:
224
319
  return False
225
320
 
@@ -292,8 +387,9 @@ class ChatOllamaComponent(LCModelComponent):
292
387
  show_url = urljoin(base_url, "api/show")
293
388
 
294
389
  async with httpx.AsyncClient() as client:
390
+ headers = self.headers
295
391
  # Fetch available models
296
- tags_response = await client.get(tags_url)
392
+ tags_response = await client.get(url=tags_url, headers=headers)
297
393
  tags_response.raise_for_status()
298
394
  models = tags_response.json()
299
395
  if asyncio.iscoroutine(models):
@@ -307,11 +403,12 @@ class ChatOllamaComponent(LCModelComponent):
307
403
  await logger.adebug(f"Checking model: {model_name}")
308
404
 
309
405
  payload = {"model": model_name}
310
- show_response = await client.post(show_url, json=payload)
406
+ show_response = await client.post(url=show_url, json=payload, headers=headers)
311
407
  show_response.raise_for_status()
312
408
  json_data = show_response.json()
313
409
  if asyncio.iscoroutine(json_data):
314
410
  json_data = await json_data
411
+
315
412
  capabilities = json_data.get(self.JSON_CAPABILITIES_KEY, [])
316
413
  await logger.adebug(f"Model: {model_name}, Capabilities: {capabilities}")
317
414
 
@@ -325,3 +422,113 @@ class ChatOllamaComponent(LCModelComponent):
325
422
  raise ValueError(msg) from e
326
423
 
327
424
  return model_ids
425
+
426
+ def _parse_format_field(self, format_value: Any) -> Any:
427
+ """Parse the format field to handle both string and dict inputs.
428
+
429
+ The format field can be:
430
+ - A simple string like "json" (backward compatibility)
431
+ - A JSON string from NestedDictInput that needs parsing
432
+ - A dict/JSON schema (already parsed)
433
+ - None or empty
434
+
435
+ Args:
436
+ format_value: The raw format value from the input field
437
+
438
+ Returns:
439
+ Parsed format value as string, dict, or None
440
+ """
441
+ if not format_value:
442
+ return None
443
+
444
+ schema = format_value
445
+ if isinstance(format_value, list):
446
+ schema = build_model_from_schema(format_value).model_json_schema()
447
+ if schema == self.default_table_row_schema:
448
+ return None # the rows are generic placeholder rows
449
+ elif isinstance(format_value, str): # parse as json if string
450
+ with suppress(json.JSONDecodeError): # e.g., literal "json" is valid for format field
451
+ schema = json.loads(format_value)
452
+
453
+ return schema or None
454
+
455
+ async def _parse_json_response(self) -> Any:
456
+ """Parse the JSON response from the model.
457
+
458
+ This method gets the text response and attempts to parse it as JSON.
459
+ Works with models that have format='json' or a JSON schema set.
460
+
461
+ Returns:
462
+ Parsed JSON (dict, list, or primitive type)
463
+
464
+ Raises:
465
+ ValueError: If the response is not valid JSON
466
+ """
467
+ message = await self.text_response()
468
+ text = message.text if hasattr(message, "text") else str(message)
469
+
470
+ if not text:
471
+ msg = "No response from model"
472
+ raise ValueError(msg)
473
+
474
+ try:
475
+ return json.loads(text)
476
+ except json.JSONDecodeError as e:
477
+ msg = f"Invalid JSON response. Ensure model supports JSON output. Error: {e}"
478
+ raise ValueError(msg) from e
479
+
480
+ async def build_data_output(self) -> Data:
481
+ """Build a Data output from the model's JSON response.
482
+
483
+ Returns:
484
+ Data: A Data object containing the parsed JSON response
485
+ """
486
+ parsed = await self._parse_json_response()
487
+
488
+ # If the response is already a dict, wrap it in Data
489
+ if isinstance(parsed, dict):
490
+ return Data(data=parsed)
491
+
492
+ # If it's a list, wrap in a results container
493
+ if isinstance(parsed, list):
494
+ if len(parsed) == 1:
495
+ return Data(data=parsed[0])
496
+ return Data(data={"results": parsed})
497
+
498
+ # For primitive types, wrap in a value container
499
+ return Data(data={"value": parsed})
500
+
501
+ async def build_dataframe_output(self) -> DataFrame:
502
+ """Build a DataFrame output from the model's JSON response.
503
+
504
+ Returns:
505
+ DataFrame: A DataFrame containing the parsed JSON response
506
+
507
+ Raises:
508
+ ValueError: If the response cannot be converted to a DataFrame
509
+ """
510
+ parsed = await self._parse_json_response()
511
+
512
+ # If it's a list of dicts, convert directly to DataFrame
513
+ if isinstance(parsed, list):
514
+ if not parsed:
515
+ return DataFrame()
516
+ # Ensure all items are dicts for proper DataFrame conversion
517
+ if all(isinstance(item, dict) for item in parsed):
518
+ return DataFrame(parsed)
519
+ msg = "List items must be dictionaries to convert to DataFrame"
520
+ raise ValueError(msg)
521
+
522
+ # If it's a single dict, wrap in a list to create a single-row DataFrame
523
+ if isinstance(parsed, dict):
524
+ return DataFrame([parsed])
525
+
526
+ # For primitive types, create a single-column DataFrame
527
+ return DataFrame([{"value": parsed}])
528
+
529
+ @property
530
+ def headers(self) -> dict[str, str] | None:
531
+ """Get the headers for the Ollama API."""
532
+ if self.api_key and self.api_key.strip():
533
+ return {"Authorization": f"Bearer {self.api_key}"}
534
+ return None