aip-agents-binary 0.5.20__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.
- aip_agents/__init__.py +65 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1086 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/agent/__init__.py +27 -0
- aip_agents/agent/base_agent.py +970 -0
- aip_agents/agent/base_langgraph_agent.py +2942 -0
- aip_agents/agent/google_adk_agent.py +926 -0
- aip_agents/agent/google_adk_constants.py +6 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
- aip_agents/agent/langgraph_react_agent.py +2514 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/constants.py +23 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +142 -0
- aip_agents/examples/demo_memory_recall.py +401 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
- aip_agents/examples/hello_world_tool_output_client.py +46 -0
- aip_agents/examples/hello_world_tool_output_server.py +114 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +126 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/executor/agent_executor.py +473 -0
- aip_agents/executor/base.py +48 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +14 -0
- aip_agents/mcp/client/base_mcp_client.py +369 -0
- aip_agents/mcp/client/connection_manager.py +193 -0
- aip_agents/mcp/client/google_adk/__init__.py +11 -0
- aip_agents/mcp/client/google_adk/client.py +381 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +359 -0
- aip_agents/mcp/client/session_pool.py +351 -0
- aip_agents/mcp/client/transports.py +215 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +717 -0
- aip_agents/memory/adapters/mem0.py +84 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +17 -0
- aip_agents/middleware/base.py +88 -0
- aip_agents/middleware/manager.py +128 -0
- aip_agents/middleware/todolist.py +274 -0
- aip_agents/schema/__init__.py +69 -0
- aip_agents/schema/a2a.py +56 -0
- aip_agents/schema/agent.py +111 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +37 -0
- aip_agents/schema/model_id.py +97 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +151 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +33 -0
- aip_agents/tools/bosa_tools.py +105 -0
- aip_agents/tools/browser_use/__init__.py +82 -0
- aip_agents/tools/browser_use/action_parser.py +103 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +198 -0
- aip_agents/tools/browser_use/schemas.py +119 -0
- aip_agents/tools/browser_use/session.py +76 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +317 -0
- aip_agents/tools/browser_use/streaming.py +813 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +78 -0
- aip_agents/tools/code_sandbox/__init__.py +26 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
- aip_agents/tools/constants.py +165 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +351 -0
- aip_agents/tools/memory_search/__init__.py +22 -0
- aip_agents/tools/memory_search/base.py +200 -0
- aip_agents/tools/memory_search/mem0.py +258 -0
- aip_agents/tools/memory_search/schema.py +48 -0
- aip_agents/tools/memory_search_tool.py +26 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +70 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +79 -0
- aip_agents/utils/a2a_connector.py +1757 -0
- aip_agents/utils/artifact_helpers.py +502 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +176 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +231 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +128 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
- aip_agents/utils/langgraph/tool_output_management.py +967 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +358 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +25 -0
- aip_agents/utils/pii/pii_handler.py +397 -0
- aip_agents/utils/pii/pii_helper.py +207 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +831 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +156 -0
- aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
- aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
- aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
- aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Utilities for constructing agent system instructions.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from aip_agents.utils.constants import DefaultTimezone
|
|
10
|
+
from aip_agents.utils.datetime import get_timezone_aware_now
|
|
11
|
+
|
|
12
|
+
__all__ = ["get_current_date_context", "DefaultTimezone"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_current_date_context(timezone: str = DefaultTimezone.JAKARTA) -> str:
|
|
16
|
+
"""Generate current date context for system prompts.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
timezone: IANA timezone name for date formatting.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Formatted date context string for inclusion in system prompts.
|
|
23
|
+
"""
|
|
24
|
+
now, timezone_label, used_fallback = get_timezone_aware_now(timezone)
|
|
25
|
+
display_timezone = "UTC" if used_fallback else timezone_label
|
|
26
|
+
current_date = now.strftime("%Y-%m-%d")
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
f"Very important: The user's timezone is {display_timezone}. "
|
|
30
|
+
f"The current date is {current_date}\n\n"
|
|
31
|
+
"Any dates before this are in the past, and any dates after this are in the future. "
|
|
32
|
+
"When the user asks for the 'latest', 'most recent', 'today's', etc. don't assume\n"
|
|
33
|
+
"your knowledge is up to date."
|
|
34
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Clients module for external API integrations.
|
|
2
|
+
|
|
3
|
+
This module contains HTTP clients for various external services and APIs,
|
|
4
|
+
providing a unified interface for communication with different platforms.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
- Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__all__ = ["langflow"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Langflow client module for HTTP communication with Langflow APIs.
|
|
2
|
+
|
|
3
|
+
This module provides the LangflowApiClient class that handles all HTTP communication
|
|
4
|
+
with Langflow APIs, including both streaming and non-streaming execution modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aip_agents.clients.langflow.client import LangflowApiClient
|
|
8
|
+
from aip_agents.clients.langflow.types import LangflowEventType
|
|
9
|
+
|
|
10
|
+
__all__ = ["LangflowApiClient", "LangflowEventType"]
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Langflow API client for HTTP communication with Langflow API.
|
|
2
|
+
|
|
3
|
+
This module provides the LangflowApiClient class that handles all HTTP communication
|
|
4
|
+
with Langflow APIs, including both streaming and non-streaming execution modes.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from aip_agents.clients.langflow.types import LangflowEventType
|
|
19
|
+
from aip_agents.schema.agent import HttpxClientOptions
|
|
20
|
+
from aip_agents.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DEFAULT_LANGFLOW_BASE_URL = os.getenv("LANGFLOW_BASE_URL", "https://langflow.obrol.id")
|
|
26
|
+
MAX_PAGE_SIZE = 1000
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LangflowApiClient:
|
|
30
|
+
"""HTTP client for Langflow API with streaming and non-streaming support.
|
|
31
|
+
|
|
32
|
+
This client handles all communication with Langflow APIs, including:
|
|
33
|
+
- Non-streaming execution
|
|
34
|
+
- Server-Sent Events (SSE) streaming
|
|
35
|
+
- Session management for conversation continuity
|
|
36
|
+
- Error handling and retries
|
|
37
|
+
- Credential management
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
flow_id: str | None = None,
|
|
43
|
+
base_url: str | None = None,
|
|
44
|
+
api_key: str | None = None,
|
|
45
|
+
httpx_client_options: "HttpxClientOptions" = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize the Langflow API client.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
flow_id: The unique identifier of the Langflow flow to execute.
|
|
51
|
+
base_url: The base URL of the Langflow API server.
|
|
52
|
+
api_key: The API key for Langflow authentication.
|
|
53
|
+
httpx_client_options: HTTP client configuration options for httpx, including timeout.
|
|
54
|
+
"""
|
|
55
|
+
self.flow_id = flow_id
|
|
56
|
+
self.base_url = self._resolve_base_url(base_url)
|
|
57
|
+
self.api_key = self._resolve_api_key(api_key)
|
|
58
|
+
self.sessions: dict[str, str] = {}
|
|
59
|
+
|
|
60
|
+
# Get client options from config or use defaults
|
|
61
|
+
if not isinstance(httpx_client_options, HttpxClientOptions):
|
|
62
|
+
client_options = HttpxClientOptions()
|
|
63
|
+
else:
|
|
64
|
+
client_options = httpx_client_options
|
|
65
|
+
|
|
66
|
+
self.client_kwargs = {
|
|
67
|
+
"timeout": httpx.Timeout(client_options.timeout),
|
|
68
|
+
"trust_env": client_options.trust_env,
|
|
69
|
+
"follow_redirects": client_options.follow_redirects,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(f"Initialized Langflow API client for flow {self.flow_id} at {self.base_url}")
|
|
73
|
+
|
|
74
|
+
def _resolve_base_url(self, base_url: str | None) -> str:
|
|
75
|
+
"""Resolve the base URL from config or environment variables.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
base_url: Base URL from config.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Resolved base URL.
|
|
82
|
+
"""
|
|
83
|
+
if base_url:
|
|
84
|
+
return base_url.rstrip("/")
|
|
85
|
+
|
|
86
|
+
return DEFAULT_LANGFLOW_BASE_URL.rstrip("/")
|
|
87
|
+
|
|
88
|
+
def _resolve_api_key(self, api_key: str | None) -> str:
|
|
89
|
+
"""Resolve the API key from config or environment variables.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
api_key: API key from config.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Resolved API key.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If no API key is found.
|
|
99
|
+
"""
|
|
100
|
+
if api_key:
|
|
101
|
+
return api_key
|
|
102
|
+
|
|
103
|
+
env_key = os.getenv("LANGFLOW_API_KEY")
|
|
104
|
+
if env_key:
|
|
105
|
+
return env_key
|
|
106
|
+
|
|
107
|
+
raise ValueError("LANGFLOW_API_KEY not found. Please provide via config or environment variable.")
|
|
108
|
+
|
|
109
|
+
def _build_url(self, flow_id: str | None = None, stream: bool = False) -> str:
|
|
110
|
+
"""Build the API URL for flow execution.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
flow_id: Optional flow ID to use in the URL. If None, uses the instance flow_id.
|
|
114
|
+
stream: Whether to build URL for streaming mode.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Complete API URL.
|
|
118
|
+
"""
|
|
119
|
+
base_flow_url = f"{self.base_url}/api/v1/run/{self._ensure_flow_id(flow_id)}"
|
|
120
|
+
if stream:
|
|
121
|
+
return f"{base_flow_url}?stream=true"
|
|
122
|
+
return base_flow_url
|
|
123
|
+
|
|
124
|
+
def _build_headers(self) -> dict[str, str]:
|
|
125
|
+
"""Build HTTP headers for API requests.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary of HTTP headers.
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"Accept": "application/json",
|
|
133
|
+
"x-api-key": self.api_key,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def _build_payload(self, input_value: str, session_id: str | None = None) -> dict[str, Any]:
|
|
137
|
+
"""Build the request payload for Langflow API.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
input_value: The user input to send to the flow.
|
|
141
|
+
session_id: Optional session ID for conversation continuity.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary containing the request payload.
|
|
145
|
+
"""
|
|
146
|
+
payload = {
|
|
147
|
+
"output_type": "chat",
|
|
148
|
+
"input_type": "chat",
|
|
149
|
+
"input_value": input_value,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if session_id:
|
|
153
|
+
payload["session_id"] = session_id
|
|
154
|
+
|
|
155
|
+
return payload
|
|
156
|
+
|
|
157
|
+
def get_or_create_session(self, thread_id: str | None = None) -> str:
|
|
158
|
+
"""Get existing session ID or create a new one.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
thread_id: Optional thread ID for session mapping.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Session ID for the conversation.
|
|
165
|
+
"""
|
|
166
|
+
if thread_id and thread_id in self.sessions:
|
|
167
|
+
return self.sessions[thread_id]
|
|
168
|
+
|
|
169
|
+
session_id = str(uuid.uuid4())
|
|
170
|
+
if thread_id:
|
|
171
|
+
self.sessions[thread_id] = session_id
|
|
172
|
+
logger.debug(f"Created new session {session_id} for thread {thread_id}")
|
|
173
|
+
else:
|
|
174
|
+
logger.debug(f"Created new session {session_id}")
|
|
175
|
+
|
|
176
|
+
return session_id
|
|
177
|
+
|
|
178
|
+
def _ensure_flow_id(self, flow_id: str | None) -> str:
|
|
179
|
+
"""Ensure the flow ID is set.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
flow_id: The flow ID to ensure.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The flow ID.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If the flow ID is not set.
|
|
189
|
+
"""
|
|
190
|
+
flow_id = flow_id or self.flow_id
|
|
191
|
+
if not flow_id:
|
|
192
|
+
raise ValueError("Flow ID is required")
|
|
193
|
+
return flow_id
|
|
194
|
+
|
|
195
|
+
async def call_flow(
|
|
196
|
+
self, input_value: str, session_id: str | None = None, flow_id: str | None = None, **_: Any
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
"""Execute Langflow flow without streaming.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
input_value: The user input to send to the flow.
|
|
202
|
+
session_id: Optional session ID for conversation continuity.
|
|
203
|
+
flow_id: Optional flow ID to execute. If None, uses the instance flow_id.
|
|
204
|
+
**_: Additional keyword arguments.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The response from the flow execution.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
httpx.HTTPError: If the HTTP request fails.
|
|
211
|
+
ValueError: If the response cannot be parsed.
|
|
212
|
+
"""
|
|
213
|
+
url = self._build_url(flow_id=flow_id, stream=False)
|
|
214
|
+
headers = self._build_headers()
|
|
215
|
+
payload = self._build_payload(input_value, session_id)
|
|
216
|
+
|
|
217
|
+
logger.debug(f"Calling flow {self._ensure_flow_id(flow_id)} with non-streaming mode")
|
|
218
|
+
|
|
219
|
+
async with httpx.AsyncClient(**self.client_kwargs) as client:
|
|
220
|
+
try:
|
|
221
|
+
response = await client.post(url, json=payload, headers=headers)
|
|
222
|
+
response.raise_for_status()
|
|
223
|
+
|
|
224
|
+
response_data = response.json()
|
|
225
|
+
|
|
226
|
+
# Return content or fallback
|
|
227
|
+
return response_data
|
|
228
|
+
|
|
229
|
+
except httpx.HTTPError as e:
|
|
230
|
+
logger.error(f"HTTP error during flow execution: {e}")
|
|
231
|
+
raise
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Error parsing flow response: {e}")
|
|
234
|
+
raise ValueError(f"Failed to parse flow response: {e}") from e
|
|
235
|
+
|
|
236
|
+
async def stream_flow(
|
|
237
|
+
self, input_value: str, session_id: str | None = None, flow_id: str | None = None, **_: Any
|
|
238
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
239
|
+
"""Execute Langflow flow with streaming.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
input_value: The user input to send to the flow.
|
|
243
|
+
session_id: Optional session ID for conversation continuity.
|
|
244
|
+
flow_id: Optional flow ID to execute. If None, uses the instance flow_id.
|
|
245
|
+
**_: Additional keyword arguments.
|
|
246
|
+
|
|
247
|
+
Yields:
|
|
248
|
+
Parsed streaming events from the Langflow API.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
httpx.HTTPError: If the HTTP request fails.
|
|
252
|
+
ValueError: If streaming events cannot be parsed.
|
|
253
|
+
"""
|
|
254
|
+
url = self._build_url(flow_id=flow_id, stream=True)
|
|
255
|
+
headers = self._build_headers()
|
|
256
|
+
payload = self._build_payload(input_value, session_id)
|
|
257
|
+
|
|
258
|
+
logger.debug(f"Calling flow {self._ensure_flow_id(flow_id)} with streaming mode")
|
|
259
|
+
|
|
260
|
+
async with httpx.AsyncClient(**self.client_kwargs) as client:
|
|
261
|
+
try:
|
|
262
|
+
async with client.stream("POST", url, json=payload, headers=headers) as response:
|
|
263
|
+
response.raise_for_status()
|
|
264
|
+
|
|
265
|
+
async for line in response.aiter_lines():
|
|
266
|
+
if line.strip():
|
|
267
|
+
try:
|
|
268
|
+
event_data = json.loads(line)
|
|
269
|
+
yield event_data
|
|
270
|
+
except json.JSONDecodeError:
|
|
271
|
+
logger.warning(f"Skipping non-JSON line: {line}")
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
except httpx.HTTPError as e:
|
|
275
|
+
logger.error(f"HTTP error during streaming: {e}")
|
|
276
|
+
raise
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Error during streaming: {e}")
|
|
279
|
+
raise
|
|
280
|
+
|
|
281
|
+
def parse_stream_event(self, event_data: dict[str, Any]) -> dict[str, Any] | None:
|
|
282
|
+
"""Parse a single streaming event from Langflow.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
event_data: Raw event data from Langflow streaming response.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Parsed event dictionary or None if event should be skipped.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
event_type = event_data.get("event")
|
|
292
|
+
|
|
293
|
+
if event_type == LangflowEventType.ADD_MESSAGE:
|
|
294
|
+
message_data = event_data.get("data", {})
|
|
295
|
+
sender = message_data.get("sender", "Unknown")
|
|
296
|
+
text = message_data.get("text", "")
|
|
297
|
+
return {"type": LangflowEventType.ADD_MESSAGE, "sender": sender, "content": text, "raw": event_data}
|
|
298
|
+
|
|
299
|
+
elif event_type == LangflowEventType.END:
|
|
300
|
+
return {
|
|
301
|
+
"type": LangflowEventType.END,
|
|
302
|
+
"content": "Stream completed",
|
|
303
|
+
"final": True,
|
|
304
|
+
"raw": event_data,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
else:
|
|
308
|
+
return {
|
|
309
|
+
"type": LangflowEventType.UNKNOWN,
|
|
310
|
+
"event_type": event_type,
|
|
311
|
+
"content": str(event_data),
|
|
312
|
+
"raw": event_data,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.warning(f"Error parsing stream event: {e}")
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def clear_session(self, thread_id: str) -> None:
|
|
320
|
+
"""Clear session for a specific thread.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
thread_id: Thread ID to clear session for.
|
|
324
|
+
"""
|
|
325
|
+
if thread_id in self.sessions:
|
|
326
|
+
del self.sessions[thread_id]
|
|
327
|
+
logger.debug(f"Cleared session for thread {thread_id}")
|
|
328
|
+
|
|
329
|
+
def clear_all_sessions(self) -> None:
|
|
330
|
+
"""Clear all stored sessions."""
|
|
331
|
+
self.sessions.clear()
|
|
332
|
+
logger.debug("Cleared all sessions")
|
|
333
|
+
|
|
334
|
+
async def get_flows( # noqa: PLR0913
|
|
335
|
+
self,
|
|
336
|
+
project_id: str | None = None,
|
|
337
|
+
remove_example_flows: bool = False,
|
|
338
|
+
components_only: bool = False,
|
|
339
|
+
header_flows: bool = False,
|
|
340
|
+
get_all: bool = True,
|
|
341
|
+
page: int = 1,
|
|
342
|
+
size: int = 50,
|
|
343
|
+
) -> list[dict[str, Any]]:
|
|
344
|
+
"""Retrieve flows from Langflow API with full control over parameters.
|
|
345
|
+
|
|
346
|
+
Based on the official API docs: https://docs.langflow.org/api-flows
|
|
347
|
+
Uses the exact parameter format from the documentation.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
project_id: Optional project ID to filter flows.
|
|
351
|
+
remove_example_flows: Whether to exclude example flows. Defaults to False.
|
|
352
|
+
components_only: Whether to return only components. Defaults to False.
|
|
353
|
+
header_flows: Whether to return only flow headers. Defaults to False.
|
|
354
|
+
get_all: Whether to return all flows (ignores pagination). Defaults to True.
|
|
355
|
+
page: Page number for pagination (ignored if get_all=True). Defaults to 1.
|
|
356
|
+
size: Number of flows per page (ignored if get_all=True). Defaults to 50.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of flows or flow headers from the Langflow API.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
httpx.HTTPError: If the HTTP request fails.
|
|
363
|
+
ValueError: If the response cannot be parsed or invalid parameters provided.
|
|
364
|
+
"""
|
|
365
|
+
self._validate_pagination_params(page, size)
|
|
366
|
+
|
|
367
|
+
url = f"{self.base_url}/api/v1/flows/"
|
|
368
|
+
headers = self._build_headers()
|
|
369
|
+
|
|
370
|
+
# Use exact parameter names from official docs
|
|
371
|
+
params = {
|
|
372
|
+
"remove_example_flows": str(remove_example_flows).lower(),
|
|
373
|
+
"components_only": str(components_only).lower(),
|
|
374
|
+
"get_all": str(get_all).lower(),
|
|
375
|
+
"header_flows": str(header_flows).lower(),
|
|
376
|
+
"page": page,
|
|
377
|
+
"size": size,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if project_id:
|
|
381
|
+
params["project_id"] = project_id
|
|
382
|
+
|
|
383
|
+
async with httpx.AsyncClient(**self.client_kwargs) as client:
|
|
384
|
+
try:
|
|
385
|
+
response = await client.get(url, headers=headers, params=params)
|
|
386
|
+
response.raise_for_status()
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
response_data = response.json()
|
|
390
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
391
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
392
|
+
logger.warning(f"Response body: {response.text[:500]}...")
|
|
393
|
+
raise ValueError(f"Failed to parse flows response: {e}") from e
|
|
394
|
+
|
|
395
|
+
if isinstance(response_data, list):
|
|
396
|
+
return response_data
|
|
397
|
+
elif isinstance(response_data, dict) and "flows" in response_data:
|
|
398
|
+
return response_data["flows"]
|
|
399
|
+
else:
|
|
400
|
+
logger.warning(f"Unexpected response format: {type(response_data)}")
|
|
401
|
+
if isinstance(response_data, dict):
|
|
402
|
+
logger.warning(f"Response keys: {list(response_data.keys())}")
|
|
403
|
+
else:
|
|
404
|
+
logger.warning("Response keys: Not a dict")
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
except httpx.HTTPError as e:
|
|
408
|
+
logger.error(f"HTTP error retrieving flows: {e}")
|
|
409
|
+
logger.error(f"Response status: {e.response.status_code if hasattr(e, 'response') else 'Unknown'}")
|
|
410
|
+
raise
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Unexpected error during flows request: {e}")
|
|
413
|
+
raise ValueError(f"Unexpected error during flows request: {e}") from e
|
|
414
|
+
|
|
415
|
+
def _validate_pagination_params(self, page: int, size: int) -> None:
|
|
416
|
+
"""Validate pagination parameters for flow retrieval.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
page: Page number for pagination.
|
|
420
|
+
size: Number of flows per page.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ValueError: If page or size parameters are invalid.
|
|
424
|
+
"""
|
|
425
|
+
if page < 1:
|
|
426
|
+
raise ValueError("page must be >= 1")
|
|
427
|
+
if size < 1:
|
|
428
|
+
raise ValueError("size must be >= 1")
|
|
429
|
+
if size > MAX_PAGE_SIZE:
|
|
430
|
+
logger.warning(f"Large page size requested: {size}")
|
|
431
|
+
|
|
432
|
+
async def get_all_flows(
|
|
433
|
+
self,
|
|
434
|
+
project_id: str | None = None,
|
|
435
|
+
remove_example_flows: bool = False,
|
|
436
|
+
components_only: bool = False,
|
|
437
|
+
header_flows: bool = False,
|
|
438
|
+
) -> list[dict[str, Any]]:
|
|
439
|
+
"""Convenience method to get ALL flows using the backend's get_all=true feature.
|
|
440
|
+
|
|
441
|
+
This method is a simple wrapper around get_flows() with get_all=True,
|
|
442
|
+
which uses the Langflow backend's ability to return all flows in one request.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
project_id: Optional project ID to filter flows.
|
|
446
|
+
remove_example_flows: Whether to exclude example flows. Defaults to False.
|
|
447
|
+
components_only: Whether to return only components. Defaults to False.
|
|
448
|
+
header_flows: Whether to return only flow headers. Defaults to False.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
List of all flows from the Langflow API.
|
|
452
|
+
|
|
453
|
+
Raises:
|
|
454
|
+
httpx.HTTPError: If the HTTP request fails.
|
|
455
|
+
ValueError: If the response cannot be parsed.
|
|
456
|
+
"""
|
|
457
|
+
return await self.get_flows(
|
|
458
|
+
project_id=project_id,
|
|
459
|
+
remove_example_flows=remove_example_flows,
|
|
460
|
+
components_only=components_only,
|
|
461
|
+
header_flows=header_flows,
|
|
462
|
+
get_all=True,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def health_check(self) -> bool:
|
|
466
|
+
"""Check if the Langflow API is accessible.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
True if the API is accessible, False otherwise.
|
|
470
|
+
"""
|
|
471
|
+
try:
|
|
472
|
+
async with httpx.AsyncClient(**self.client_kwargs) as client:
|
|
473
|
+
response = await client.get(f"{self.base_url}/health", headers={"x-api-key": self.api_key})
|
|
474
|
+
return response.status_code == httpx.codes.OK
|
|
475
|
+
except Exception as e:
|
|
476
|
+
logger.warning(f"Health check failed: {e}")
|
|
477
|
+
return False
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Types and enums for Langflow client module.
|
|
2
|
+
|
|
3
|
+
This module contains type definitions, enums, and data structures
|
|
4
|
+
specific to Langflow API communication and event handling.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LangflowEventType(StrEnum):
|
|
14
|
+
"""Enum for Langflow event types as received from the API."""
|
|
15
|
+
|
|
16
|
+
ADD_MESSAGE = "add_message"
|
|
17
|
+
END = "end"
|
|
18
|
+
UNKNOWN = "unknown"
|
aip_agents/constants.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Generic Constants for AIP Agents components.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# Base directory of the project
|
|
10
|
+
# Note: This might need adjustment depending on how aip_agents is used as a library.
|
|
11
|
+
# Consider if this is truly needed in the generic package.
|
|
12
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
|
|
14
|
+
# Agent settings
|
|
15
|
+
DEFAULT_AGENT_TIMEOUT = 120 # seconds
|
|
16
|
+
|
|
17
|
+
# Chat history settings - These might also be application-specific,
|
|
18
|
+
# but are kept here for now as they are often used by generic agent logic.
|
|
19
|
+
LAST_N_CHATS = 10 # Number of previous chat pairs to include in history
|
|
20
|
+
MAX_MEMORY_TOKENS = 32000 # Maximum tokens allowed in memory
|
|
21
|
+
|
|
22
|
+
# Logging settings
|
|
23
|
+
TEXT_PREVIEW_LENGTH = 50 # Maximum length for text previews in logs
|