ag2 0.9.1__py3-none-any.whl → 0.9.1.post0__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.
Potentially problematic release.
This version of ag2 might be problematic. Click here for more details.
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info}/METADATA +264 -73
- ag2-0.9.1.post0.dist-info/RECORD +392 -0
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info}/WHEEL +1 -2
- autogen/__init__.py +89 -0
- autogen/_website/__init__.py +3 -0
- autogen/_website/generate_api_references.py +427 -0
- autogen/_website/generate_mkdocs.py +1174 -0
- autogen/_website/notebook_processor.py +476 -0
- autogen/_website/process_notebooks.py +656 -0
- autogen/_website/utils.py +412 -0
- autogen/agentchat/__init__.py +44 -0
- autogen/agentchat/agent.py +182 -0
- autogen/agentchat/assistant_agent.py +85 -0
- autogen/agentchat/chat.py +309 -0
- autogen/agentchat/contrib/__init__.py +5 -0
- autogen/agentchat/contrib/agent_eval/README.md +7 -0
- autogen/agentchat/contrib/agent_eval/agent_eval.py +108 -0
- autogen/agentchat/contrib/agent_eval/criterion.py +43 -0
- autogen/agentchat/contrib/agent_eval/critic_agent.py +44 -0
- autogen/agentchat/contrib/agent_eval/quantifier_agent.py +39 -0
- autogen/agentchat/contrib/agent_eval/subcritic_agent.py +45 -0
- autogen/agentchat/contrib/agent_eval/task.py +42 -0
- autogen/agentchat/contrib/agent_optimizer.py +429 -0
- autogen/agentchat/contrib/capabilities/__init__.py +5 -0
- autogen/agentchat/contrib/capabilities/agent_capability.py +20 -0
- autogen/agentchat/contrib/capabilities/generate_images.py +301 -0
- autogen/agentchat/contrib/capabilities/teachability.py +393 -0
- autogen/agentchat/contrib/capabilities/text_compressors.py +66 -0
- autogen/agentchat/contrib/capabilities/tools_capability.py +22 -0
- autogen/agentchat/contrib/capabilities/transform_messages.py +93 -0
- autogen/agentchat/contrib/capabilities/transforms.py +566 -0
- autogen/agentchat/contrib/capabilities/transforms_util.py +122 -0
- autogen/agentchat/contrib/capabilities/vision_capability.py +214 -0
- autogen/agentchat/contrib/captainagent/__init__.py +9 -0
- autogen/agentchat/contrib/captainagent/agent_builder.py +790 -0
- autogen/agentchat/contrib/captainagent/captainagent.py +512 -0
- autogen/agentchat/contrib/captainagent/tool_retriever.py +335 -0
- autogen/agentchat/contrib/captainagent/tools/README.md +44 -0
- autogen/agentchat/contrib/captainagent/tools/__init__.py +5 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_correlation.py +40 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_skewness_and_kurtosis.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_iqr.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_zscore.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/explore_csv.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/shapiro_wilk_test.py +30 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_download.py +27 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_search.py +53 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_image.py +53 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_text.py +38 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_wikipedia_text.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_youtube_caption.py +34 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/image_qa.py +60 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/optical_character_recognition.py +61 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/perform_web_search.py +47 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/scrape_wikipedia_tables.py +33 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/transcribe_audio_file.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/youtube_download.py +35 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_circle_area_from_diameter.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_day_of_the_week.py +18 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_fraction_sum.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_matrix_power.py +31 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_reflected_point.py +16 -0
- autogen/agentchat/contrib/captainagent/tools/math/complex_numbers_product.py +25 -0
- autogen/agentchat/contrib/captainagent/tools/math/compute_currency_conversion.py +23 -0
- autogen/agentchat/contrib/captainagent/tools/math/count_distinct_permutations.py +27 -0
- autogen/agentchat/contrib/captainagent/tools/math/evaluate_expression.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/math/find_continuity_point.py +34 -0
- autogen/agentchat/contrib/captainagent/tools/math/fraction_to_mixed_numbers.py +39 -0
- autogen/agentchat/contrib/captainagent/tools/math/modular_inverse_sum.py +23 -0
- autogen/agentchat/contrib/captainagent/tools/math/simplify_mixed_numbers.py +36 -0
- autogen/agentchat/contrib/captainagent/tools/math/sum_of_digit_factorials.py +15 -0
- autogen/agentchat/contrib/captainagent/tools/math/sum_of_primes_below.py +15 -0
- autogen/agentchat/contrib/captainagent/tools/requirements.txt +10 -0
- autogen/agentchat/contrib/captainagent/tools/tool_description.tsv +34 -0
- autogen/agentchat/contrib/gpt_assistant_agent.py +526 -0
- autogen/agentchat/contrib/graph_rag/__init__.py +9 -0
- autogen/agentchat/contrib/graph_rag/document.py +29 -0
- autogen/agentchat/contrib/graph_rag/falkor_graph_query_engine.py +170 -0
- autogen/agentchat/contrib/graph_rag/falkor_graph_rag_capability.py +103 -0
- autogen/agentchat/contrib/graph_rag/graph_query_engine.py +53 -0
- autogen/agentchat/contrib/graph_rag/graph_rag_capability.py +63 -0
- autogen/agentchat/contrib/graph_rag/neo4j_graph_query_engine.py +268 -0
- autogen/agentchat/contrib/graph_rag/neo4j_graph_rag_capability.py +83 -0
- autogen/agentchat/contrib/graph_rag/neo4j_native_graph_query_engine.py +210 -0
- autogen/agentchat/contrib/graph_rag/neo4j_native_graph_rag_capability.py +93 -0
- autogen/agentchat/contrib/img_utils.py +397 -0
- autogen/agentchat/contrib/llamaindex_conversable_agent.py +117 -0
- autogen/agentchat/contrib/llava_agent.py +187 -0
- autogen/agentchat/contrib/math_user_proxy_agent.py +464 -0
- autogen/agentchat/contrib/multimodal_conversable_agent.py +125 -0
- autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py +324 -0
- autogen/agentchat/contrib/rag/__init__.py +10 -0
- autogen/agentchat/contrib/rag/chromadb_query_engine.py +272 -0
- autogen/agentchat/contrib/rag/llamaindex_query_engine.py +198 -0
- autogen/agentchat/contrib/rag/mongodb_query_engine.py +329 -0
- autogen/agentchat/contrib/rag/query_engine.py +74 -0
- autogen/agentchat/contrib/retrieve_assistant_agent.py +56 -0
- autogen/agentchat/contrib/retrieve_user_proxy_agent.py +703 -0
- autogen/agentchat/contrib/society_of_mind_agent.py +199 -0
- autogen/agentchat/contrib/swarm_agent.py +1425 -0
- autogen/agentchat/contrib/text_analyzer_agent.py +79 -0
- autogen/agentchat/contrib/vectordb/__init__.py +5 -0
- autogen/agentchat/contrib/vectordb/base.py +232 -0
- autogen/agentchat/contrib/vectordb/chromadb.py +315 -0
- autogen/agentchat/contrib/vectordb/couchbase.py +407 -0
- autogen/agentchat/contrib/vectordb/mongodb.py +550 -0
- autogen/agentchat/contrib/vectordb/pgvectordb.py +928 -0
- autogen/agentchat/contrib/vectordb/qdrant.py +320 -0
- autogen/agentchat/contrib/vectordb/utils.py +126 -0
- autogen/agentchat/contrib/web_surfer.py +303 -0
- autogen/agentchat/conversable_agent.py +4020 -0
- autogen/agentchat/group/__init__.py +64 -0
- autogen/agentchat/group/available_condition.py +91 -0
- autogen/agentchat/group/context_condition.py +77 -0
- autogen/agentchat/group/context_expression.py +238 -0
- autogen/agentchat/group/context_str.py +41 -0
- autogen/agentchat/group/context_variables.py +192 -0
- autogen/agentchat/group/group_tool_executor.py +202 -0
- autogen/agentchat/group/group_utils.py +591 -0
- autogen/agentchat/group/handoffs.py +244 -0
- autogen/agentchat/group/llm_condition.py +93 -0
- autogen/agentchat/group/multi_agent_chat.py +237 -0
- autogen/agentchat/group/on_condition.py +58 -0
- autogen/agentchat/group/on_context_condition.py +54 -0
- autogen/agentchat/group/patterns/__init__.py +18 -0
- autogen/agentchat/group/patterns/auto.py +159 -0
- autogen/agentchat/group/patterns/manual.py +176 -0
- autogen/agentchat/group/patterns/pattern.py +288 -0
- autogen/agentchat/group/patterns/random.py +106 -0
- autogen/agentchat/group/patterns/round_robin.py +117 -0
- autogen/agentchat/group/reply_result.py +26 -0
- autogen/agentchat/group/speaker_selection_result.py +41 -0
- autogen/agentchat/group/targets/__init__.py +4 -0
- autogen/agentchat/group/targets/group_chat_target.py +132 -0
- autogen/agentchat/group/targets/group_manager_target.py +151 -0
- autogen/agentchat/group/targets/transition_target.py +413 -0
- autogen/agentchat/group/targets/transition_utils.py +6 -0
- autogen/agentchat/groupchat.py +1694 -0
- autogen/agentchat/realtime/__init__.py +3 -0
- autogen/agentchat/realtime/experimental/__init__.py +20 -0
- autogen/agentchat/realtime/experimental/audio_adapters/__init__.py +8 -0
- autogen/agentchat/realtime/experimental/audio_adapters/twilio_audio_adapter.py +148 -0
- autogen/agentchat/realtime/experimental/audio_adapters/websocket_audio_adapter.py +139 -0
- autogen/agentchat/realtime/experimental/audio_observer.py +42 -0
- autogen/agentchat/realtime/experimental/clients/__init__.py +15 -0
- autogen/agentchat/realtime/experimental/clients/gemini/__init__.py +7 -0
- autogen/agentchat/realtime/experimental/clients/gemini/client.py +274 -0
- autogen/agentchat/realtime/experimental/clients/oai/__init__.py +8 -0
- autogen/agentchat/realtime/experimental/clients/oai/base_client.py +220 -0
- autogen/agentchat/realtime/experimental/clients/oai/rtc_client.py +243 -0
- autogen/agentchat/realtime/experimental/clients/oai/utils.py +48 -0
- autogen/agentchat/realtime/experimental/clients/realtime_client.py +190 -0
- autogen/agentchat/realtime/experimental/function_observer.py +85 -0
- autogen/agentchat/realtime/experimental/realtime_agent.py +158 -0
- autogen/agentchat/realtime/experimental/realtime_events.py +42 -0
- autogen/agentchat/realtime/experimental/realtime_observer.py +100 -0
- autogen/agentchat/realtime/experimental/realtime_swarm.py +475 -0
- autogen/agentchat/realtime/experimental/websockets.py +21 -0
- autogen/agentchat/realtime_agent/__init__.py +21 -0
- autogen/agentchat/user_proxy_agent.py +111 -0
- autogen/agentchat/utils.py +206 -0
- autogen/agents/__init__.py +3 -0
- autogen/agents/contrib/__init__.py +10 -0
- autogen/agents/contrib/time/__init__.py +8 -0
- autogen/agents/contrib/time/time_reply_agent.py +73 -0
- autogen/agents/contrib/time/time_tool_agent.py +51 -0
- autogen/agents/experimental/__init__.py +27 -0
- autogen/agents/experimental/deep_research/__init__.py +7 -0
- autogen/agents/experimental/deep_research/deep_research.py +52 -0
- autogen/agents/experimental/discord/__init__.py +7 -0
- autogen/agents/experimental/discord/discord.py +66 -0
- autogen/agents/experimental/document_agent/__init__.py +19 -0
- autogen/agents/experimental/document_agent/chroma_query_engine.py +316 -0
- autogen/agents/experimental/document_agent/docling_doc_ingest_agent.py +118 -0
- autogen/agents/experimental/document_agent/document_agent.py +461 -0
- autogen/agents/experimental/document_agent/document_conditions.py +50 -0
- autogen/agents/experimental/document_agent/document_utils.py +380 -0
- autogen/agents/experimental/document_agent/inmemory_query_engine.py +220 -0
- autogen/agents/experimental/document_agent/parser_utils.py +130 -0
- autogen/agents/experimental/document_agent/url_utils.py +426 -0
- autogen/agents/experimental/reasoning/__init__.py +7 -0
- autogen/agents/experimental/reasoning/reasoning_agent.py +1178 -0
- autogen/agents/experimental/slack/__init__.py +7 -0
- autogen/agents/experimental/slack/slack.py +73 -0
- autogen/agents/experimental/telegram/__init__.py +7 -0
- autogen/agents/experimental/telegram/telegram.py +77 -0
- autogen/agents/experimental/websurfer/__init__.py +7 -0
- autogen/agents/experimental/websurfer/websurfer.py +62 -0
- autogen/agents/experimental/wikipedia/__init__.py +7 -0
- autogen/agents/experimental/wikipedia/wikipedia.py +90 -0
- autogen/browser_utils.py +309 -0
- autogen/cache/__init__.py +10 -0
- autogen/cache/abstract_cache_base.py +75 -0
- autogen/cache/cache.py +203 -0
- autogen/cache/cache_factory.py +88 -0
- autogen/cache/cosmos_db_cache.py +144 -0
- autogen/cache/disk_cache.py +102 -0
- autogen/cache/in_memory_cache.py +58 -0
- autogen/cache/redis_cache.py +123 -0
- autogen/code_utils.py +596 -0
- autogen/coding/__init__.py +22 -0
- autogen/coding/base.py +119 -0
- autogen/coding/docker_commandline_code_executor.py +268 -0
- autogen/coding/factory.py +47 -0
- autogen/coding/func_with_reqs.py +202 -0
- autogen/coding/jupyter/__init__.py +23 -0
- autogen/coding/jupyter/base.py +36 -0
- autogen/coding/jupyter/docker_jupyter_server.py +167 -0
- autogen/coding/jupyter/embedded_ipython_code_executor.py +182 -0
- autogen/coding/jupyter/import_utils.py +82 -0
- autogen/coding/jupyter/jupyter_client.py +231 -0
- autogen/coding/jupyter/jupyter_code_executor.py +160 -0
- autogen/coding/jupyter/local_jupyter_server.py +172 -0
- autogen/coding/local_commandline_code_executor.py +405 -0
- autogen/coding/markdown_code_extractor.py +45 -0
- autogen/coding/utils.py +56 -0
- autogen/doc_utils.py +34 -0
- autogen/events/__init__.py +7 -0
- autogen/events/agent_events.py +1010 -0
- autogen/events/base_event.py +99 -0
- autogen/events/client_events.py +167 -0
- autogen/events/helpers.py +36 -0
- autogen/events/print_event.py +46 -0
- autogen/exception_utils.py +73 -0
- autogen/extensions/__init__.py +5 -0
- autogen/fast_depends/__init__.py +16 -0
- autogen/fast_depends/_compat.py +80 -0
- autogen/fast_depends/core/__init__.py +14 -0
- autogen/fast_depends/core/build.py +225 -0
- autogen/fast_depends/core/model.py +576 -0
- autogen/fast_depends/dependencies/__init__.py +15 -0
- autogen/fast_depends/dependencies/model.py +29 -0
- autogen/fast_depends/dependencies/provider.py +39 -0
- autogen/fast_depends/library/__init__.py +10 -0
- autogen/fast_depends/library/model.py +46 -0
- autogen/fast_depends/py.typed +6 -0
- autogen/fast_depends/schema.py +66 -0
- autogen/fast_depends/use.py +280 -0
- autogen/fast_depends/utils.py +187 -0
- autogen/formatting_utils.py +83 -0
- autogen/function_utils.py +13 -0
- autogen/graph_utils.py +178 -0
- autogen/import_utils.py +526 -0
- autogen/interop/__init__.py +22 -0
- autogen/interop/crewai/__init__.py +7 -0
- autogen/interop/crewai/crewai.py +88 -0
- autogen/interop/interoperability.py +71 -0
- autogen/interop/interoperable.py +46 -0
- autogen/interop/langchain/__init__.py +8 -0
- autogen/interop/langchain/langchain_chat_model_factory.py +155 -0
- autogen/interop/langchain/langchain_tool.py +82 -0
- autogen/interop/litellm/__init__.py +7 -0
- autogen/interop/litellm/litellm_config_factory.py +113 -0
- autogen/interop/pydantic_ai/__init__.py +7 -0
- autogen/interop/pydantic_ai/pydantic_ai.py +168 -0
- autogen/interop/registry.py +69 -0
- autogen/io/__init__.py +15 -0
- autogen/io/base.py +151 -0
- autogen/io/console.py +56 -0
- autogen/io/processors/__init__.py +12 -0
- autogen/io/processors/base.py +21 -0
- autogen/io/processors/console_event_processor.py +56 -0
- autogen/io/run_response.py +293 -0
- autogen/io/thread_io_stream.py +63 -0
- autogen/io/websockets.py +213 -0
- autogen/json_utils.py +43 -0
- autogen/llm_config.py +379 -0
- autogen/logger/__init__.py +11 -0
- autogen/logger/base_logger.py +128 -0
- autogen/logger/file_logger.py +261 -0
- autogen/logger/logger_factory.py +42 -0
- autogen/logger/logger_utils.py +57 -0
- autogen/logger/sqlite_logger.py +523 -0
- autogen/math_utils.py +339 -0
- autogen/mcp/__init__.py +7 -0
- autogen/mcp/mcp_client.py +208 -0
- autogen/messages/__init__.py +7 -0
- autogen/messages/agent_messages.py +948 -0
- autogen/messages/base_message.py +107 -0
- autogen/messages/client_messages.py +171 -0
- autogen/messages/print_message.py +49 -0
- autogen/oai/__init__.py +53 -0
- autogen/oai/anthropic.py +714 -0
- autogen/oai/bedrock.py +628 -0
- autogen/oai/cerebras.py +299 -0
- autogen/oai/client.py +1435 -0
- autogen/oai/client_utils.py +169 -0
- autogen/oai/cohere.py +479 -0
- autogen/oai/gemini.py +990 -0
- autogen/oai/gemini_types.py +129 -0
- autogen/oai/groq.py +305 -0
- autogen/oai/mistral.py +303 -0
- autogen/oai/oai_models/__init__.py +11 -0
- autogen/oai/oai_models/_models.py +16 -0
- autogen/oai/oai_models/chat_completion.py +87 -0
- autogen/oai/oai_models/chat_completion_audio.py +32 -0
- autogen/oai/oai_models/chat_completion_message.py +86 -0
- autogen/oai/oai_models/chat_completion_message_tool_call.py +37 -0
- autogen/oai/oai_models/chat_completion_token_logprob.py +63 -0
- autogen/oai/oai_models/completion_usage.py +60 -0
- autogen/oai/ollama.py +643 -0
- autogen/oai/openai_utils.py +881 -0
- autogen/oai/together.py +370 -0
- autogen/retrieve_utils.py +491 -0
- autogen/runtime_logging.py +160 -0
- autogen/token_count_utils.py +267 -0
- autogen/tools/__init__.py +20 -0
- autogen/tools/contrib/__init__.py +9 -0
- autogen/tools/contrib/time/__init__.py +7 -0
- autogen/tools/contrib/time/time.py +41 -0
- autogen/tools/dependency_injection.py +254 -0
- autogen/tools/experimental/__init__.py +43 -0
- autogen/tools/experimental/browser_use/__init__.py +7 -0
- autogen/tools/experimental/browser_use/browser_use.py +161 -0
- autogen/tools/experimental/crawl4ai/__init__.py +7 -0
- autogen/tools/experimental/crawl4ai/crawl4ai.py +153 -0
- autogen/tools/experimental/deep_research/__init__.py +7 -0
- autogen/tools/experimental/deep_research/deep_research.py +328 -0
- autogen/tools/experimental/duckduckgo/__init__.py +7 -0
- autogen/tools/experimental/duckduckgo/duckduckgo_search.py +109 -0
- autogen/tools/experimental/google/__init__.py +14 -0
- autogen/tools/experimental/google/authentication/__init__.py +11 -0
- autogen/tools/experimental/google/authentication/credentials_hosted_provider.py +43 -0
- autogen/tools/experimental/google/authentication/credentials_local_provider.py +91 -0
- autogen/tools/experimental/google/authentication/credentials_provider.py +35 -0
- autogen/tools/experimental/google/drive/__init__.py +9 -0
- autogen/tools/experimental/google/drive/drive_functions.py +124 -0
- autogen/tools/experimental/google/drive/toolkit.py +88 -0
- autogen/tools/experimental/google/model.py +17 -0
- autogen/tools/experimental/google/toolkit_protocol.py +19 -0
- autogen/tools/experimental/google_search/__init__.py +8 -0
- autogen/tools/experimental/google_search/google_search.py +93 -0
- autogen/tools/experimental/google_search/youtube_search.py +181 -0
- autogen/tools/experimental/messageplatform/__init__.py +17 -0
- autogen/tools/experimental/messageplatform/discord/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/discord/discord.py +288 -0
- autogen/tools/experimental/messageplatform/slack/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/slack/slack.py +391 -0
- autogen/tools/experimental/messageplatform/telegram/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/telegram/telegram.py +275 -0
- autogen/tools/experimental/perplexity/__init__.py +7 -0
- autogen/tools/experimental/perplexity/perplexity_search.py +260 -0
- autogen/tools/experimental/tavily/__init__.py +7 -0
- autogen/tools/experimental/tavily/tavily_search.py +183 -0
- autogen/tools/experimental/web_search_preview/__init__.py +7 -0
- autogen/tools/experimental/web_search_preview/web_search_preview.py +114 -0
- autogen/tools/experimental/wikipedia/__init__.py +7 -0
- autogen/tools/experimental/wikipedia/wikipedia.py +287 -0
- autogen/tools/function_utils.py +411 -0
- autogen/tools/tool.py +187 -0
- autogen/tools/toolkit.py +86 -0
- autogen/types.py +29 -0
- autogen/version.py +7 -0
- ag2-0.9.1.dist-info/RECORD +0 -6
- ag2-0.9.1.dist-info/top_level.txt +0 -1
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info/licenses}/LICENSE +0 -0
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info/licenses}/NOTICE.md +0 -0
autogen/oai/ollama.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
# Portions derived from https://github.com/microsoft/autogen are under the MIT License.
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
"""Create an OpenAI-compatible client using Ollama's API.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
```python
|
|
11
|
+
llm_config = {"config_list": [{"api_type": "ollama", "model": "mistral:7b-instruct-v0.3-q6_K"}]}
|
|
12
|
+
|
|
13
|
+
agent = autogen.AssistantAgent("my_agent", llm_config=llm_config)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Install Ollama's python library using: pip install --upgrade ollama
|
|
17
|
+
Install fix-busted-json library: pip install --upgrade fix-busted-json
|
|
18
|
+
|
|
19
|
+
Resources:
|
|
20
|
+
- https://github.com/ollama/ollama-python
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import copy
|
|
26
|
+
import json
|
|
27
|
+
import random
|
|
28
|
+
import re
|
|
29
|
+
import time
|
|
30
|
+
import warnings
|
|
31
|
+
from typing import Any, Literal, Optional, Union
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
34
|
+
|
|
35
|
+
from ..import_utils import optional_import_block, require_optional_import
|
|
36
|
+
from ..llm_config import LLMConfigEntry, register_llm_config
|
|
37
|
+
from .client_utils import FormatterProtocol, should_hide_tools, validate_parameter
|
|
38
|
+
from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage
|
|
39
|
+
|
|
40
|
+
with optional_import_block():
|
|
41
|
+
import ollama
|
|
42
|
+
from fix_busted_json import repair_json
|
|
43
|
+
from ollama import Client
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_llm_config
|
|
47
|
+
class OllamaLLMConfigEntry(LLMConfigEntry):
|
|
48
|
+
api_type: Literal["ollama"] = "ollama"
|
|
49
|
+
client_host: Optional[HttpUrl] = None
|
|
50
|
+
stream: bool = False
|
|
51
|
+
num_predict: int = Field(
|
|
52
|
+
default=-1,
|
|
53
|
+
description="Maximum number of tokens to predict, note: -1 is infinite (default), -2 is fill context.",
|
|
54
|
+
)
|
|
55
|
+
num_ctx: int = Field(default=2048)
|
|
56
|
+
repeat_penalty: float = Field(default=1.1)
|
|
57
|
+
seed: int = Field(default=0)
|
|
58
|
+
temperature: float = Field(default=0.8)
|
|
59
|
+
top_k: int = Field(default=40)
|
|
60
|
+
top_p: float = Field(default=0.9)
|
|
61
|
+
hide_tools: Literal["if_all_run", "if_any_run", "never"] = "never"
|
|
62
|
+
|
|
63
|
+
def create_client(self):
|
|
64
|
+
raise NotImplementedError("OllamaLLMConfigEntry.create_client is not implemented.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OllamaClient:
|
|
68
|
+
"""Client for Ollama's API."""
|
|
69
|
+
|
|
70
|
+
# Defaults for manual tool calling
|
|
71
|
+
# Instruction is added to the first system message and provides directions to follow a two step
|
|
72
|
+
# process
|
|
73
|
+
# 1. (before tools have been called) Return JSON with the functions to call
|
|
74
|
+
# 2. (directly after tools have been called) Return Text describing the results of the function calls in text format
|
|
75
|
+
|
|
76
|
+
# Override using "manual_tool_call_instruction" config parameter
|
|
77
|
+
TOOL_CALL_MANUAL_INSTRUCTION = (
|
|
78
|
+
"You are to follow a strict two step process that will occur over "
|
|
79
|
+
"a number of interactions, so pay attention to what step you are in based on the full "
|
|
80
|
+
"conversation. We will be taking turns so only do one step at a time so don't perform step "
|
|
81
|
+
"2 until step 1 is complete and I've told you the result. The first step is to choose one "
|
|
82
|
+
"or more functions based on the request given and return only JSON with the functions and "
|
|
83
|
+
"arguments to use. The second step is to analyse the given output of the function and summarise "
|
|
84
|
+
"it returning only TEXT and not Python or JSON. "
|
|
85
|
+
"For argument values, be sure numbers aren't strings, they should not have double quotes around them. "
|
|
86
|
+
"In terms of your response format, for step 1 return only JSON and NO OTHER text, "
|
|
87
|
+
"for step 2 return only text and NO JSON/Python/Markdown. "
|
|
88
|
+
'The format for running a function is [{"name": "function_name1", "arguments":{"argument_name": "argument_value"}},{"name": "function_name2", "arguments":{"argument_name": "argument_value"}}] '
|
|
89
|
+
'Make sure the keys "name" and "arguments" are as described. '
|
|
90
|
+
"If you don't get the format correct, try again. "
|
|
91
|
+
"The following functions are available to you:[FUNCTIONS_LIST]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Appended to the last user message if no tools have been called
|
|
95
|
+
# Override using "manual_tool_call_step1" config parameter
|
|
96
|
+
TOOL_CALL_MANUAL_STEP1 = " (proceed with step 1)"
|
|
97
|
+
|
|
98
|
+
# Appended to the user message after tools have been executed. Will create a 'user' message if one doesn't exist.
|
|
99
|
+
# Override using "manual_tool_call_step2" config parameter
|
|
100
|
+
TOOL_CALL_MANUAL_STEP2 = " (proceed with step 2)"
|
|
101
|
+
|
|
102
|
+
def __init__(self, response_format: Optional[Union[BaseModel, dict[str, Any]]] = None, **kwargs):
|
|
103
|
+
"""Note that no api_key or environment variable is required for Ollama."""
|
|
104
|
+
|
|
105
|
+
# Store the response format, if provided (for structured outputs)
|
|
106
|
+
self._response_format: Optional[Union[BaseModel, dict[str, Any]]] = response_format
|
|
107
|
+
|
|
108
|
+
def message_retrieval(self, response) -> list:
|
|
109
|
+
"""Retrieve and return a list of strings or a list of Choice.Message from the response.
|
|
110
|
+
|
|
111
|
+
NOTE: if a list of Choice.Message is returned, it currently needs to contain the fields of OpenAI's ChatCompletion Message object,
|
|
112
|
+
since that is expected for function or tool calling in the rest of the codebase at the moment, unless a custom agent is being used.
|
|
113
|
+
"""
|
|
114
|
+
return [choice.message for choice in response.choices]
|
|
115
|
+
|
|
116
|
+
def cost(self, response) -> float:
|
|
117
|
+
return response.cost
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def get_usage(response) -> dict:
|
|
121
|
+
"""Return usage summary of the response using RESPONSE_USAGE_KEYS."""
|
|
122
|
+
# ... # pragma: no cover
|
|
123
|
+
return {
|
|
124
|
+
"prompt_tokens": response.usage.prompt_tokens,
|
|
125
|
+
"completion_tokens": response.usage.completion_tokens,
|
|
126
|
+
"total_tokens": response.usage.total_tokens,
|
|
127
|
+
"cost": response.cost,
|
|
128
|
+
"model": response.model,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def parse_params(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
132
|
+
"""Loads the parameters for Ollama API from the passed in parameters and returns a validated set. Checks types, ranges, and sets defaults"""
|
|
133
|
+
ollama_params = {}
|
|
134
|
+
|
|
135
|
+
# Check that we have what we need to use Ollama's API
|
|
136
|
+
# https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-completion
|
|
137
|
+
|
|
138
|
+
# The main parameters are model, prompt, stream, and options
|
|
139
|
+
# Options is a dictionary of parameters for the model
|
|
140
|
+
# There are other, advanced, parameters such as format, system (to override system message), template, raw, etc. - not used
|
|
141
|
+
|
|
142
|
+
# We won't enforce the available models
|
|
143
|
+
ollama_params["model"] = params.get("model")
|
|
144
|
+
assert ollama_params["model"], (
|
|
145
|
+
"Please specify the 'model' in your config list entry to nominate the Ollama model to use."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
ollama_params["stream"] = validate_parameter(params, "stream", bool, True, False, None, None)
|
|
149
|
+
|
|
150
|
+
# Build up the options dictionary
|
|
151
|
+
# https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values
|
|
152
|
+
options_dict = {}
|
|
153
|
+
|
|
154
|
+
if "num_predict" in params:
|
|
155
|
+
# Maximum number of tokens to predict, note: -1 is infinite, -2 is fill context, 128 is default
|
|
156
|
+
options_dict["num_predict"] = validate_parameter(params, "num_predict", int, False, 128, None, None)
|
|
157
|
+
|
|
158
|
+
if "num_ctx" in params:
|
|
159
|
+
# Set size of context window used to generate next token, 2048 is default
|
|
160
|
+
options_dict["num_ctx"] = validate_parameter(params, "num_ctx", int, False, 2048, None, None)
|
|
161
|
+
|
|
162
|
+
if "repeat_penalty" in params:
|
|
163
|
+
options_dict["repeat_penalty"] = validate_parameter(
|
|
164
|
+
params, "repeat_penalty", (int, float), False, 1.1, None, None
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if "seed" in params:
|
|
168
|
+
options_dict["seed"] = validate_parameter(params, "seed", int, False, 42, None, None)
|
|
169
|
+
|
|
170
|
+
if "temperature" in params:
|
|
171
|
+
options_dict["temperature"] = validate_parameter(
|
|
172
|
+
params, "temperature", (int, float), False, 0.8, None, None
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if "top_k" in params:
|
|
176
|
+
options_dict["top_k"] = validate_parameter(params, "top_k", int, False, 40, None, None)
|
|
177
|
+
|
|
178
|
+
if "top_p" in params:
|
|
179
|
+
options_dict["top_p"] = validate_parameter(params, "top_p", (int, float), False, 0.9, None, None)
|
|
180
|
+
|
|
181
|
+
if self._native_tool_calls and self._tools_in_conversation and not self._should_hide_tools:
|
|
182
|
+
ollama_params["tools"] = params["tools"]
|
|
183
|
+
|
|
184
|
+
# Ollama doesn't support streaming with tools natively
|
|
185
|
+
if ollama_params["stream"] and self._native_tool_calls:
|
|
186
|
+
warnings.warn(
|
|
187
|
+
"Streaming is not supported when using tools and 'Native' tool calling, streaming will be disabled.",
|
|
188
|
+
UserWarning,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
ollama_params["stream"] = False
|
|
192
|
+
|
|
193
|
+
if not self._native_tool_calls and self._tools_in_conversation:
|
|
194
|
+
# For manual tool calling we have injected the available tools into the prompt
|
|
195
|
+
# and we don't want to force JSON mode
|
|
196
|
+
ollama_params["format"] = "" # Don't force JSON for manual tool calling mode
|
|
197
|
+
|
|
198
|
+
if len(options_dict) != 0:
|
|
199
|
+
ollama_params["options"] = options_dict
|
|
200
|
+
|
|
201
|
+
# Structured outputs (see https://ollama.com/blog/structured-outputs)
|
|
202
|
+
if not self._response_format and params.get("response_format"):
|
|
203
|
+
self._response_format = params["response_format"]
|
|
204
|
+
|
|
205
|
+
if self._response_format:
|
|
206
|
+
if isinstance(self._response_format, dict):
|
|
207
|
+
ollama_params["format"] = self._response_format
|
|
208
|
+
else:
|
|
209
|
+
# Keep self._response_format as a Pydantic model for when process the response
|
|
210
|
+
ollama_params["format"] = self._response_format.model_json_schema()
|
|
211
|
+
|
|
212
|
+
return ollama_params
|
|
213
|
+
|
|
214
|
+
@require_optional_import(["ollama", "fix_busted_json"], "ollama")
|
|
215
|
+
def create(self, params: dict) -> ChatCompletion:
|
|
216
|
+
messages = params.get("messages", [])
|
|
217
|
+
|
|
218
|
+
# Are tools involved in this conversation?
|
|
219
|
+
self._tools_in_conversation = "tools" in params
|
|
220
|
+
|
|
221
|
+
# We provide second-level filtering out of tools to avoid LLMs re-calling tools continuously
|
|
222
|
+
if self._tools_in_conversation:
|
|
223
|
+
hide_tools = validate_parameter(
|
|
224
|
+
params, "hide_tools", str, False, "never", None, ["if_all_run", "if_any_run", "never"]
|
|
225
|
+
)
|
|
226
|
+
self._should_hide_tools = should_hide_tools(messages, params["tools"], hide_tools)
|
|
227
|
+
else:
|
|
228
|
+
self._should_hide_tools = False
|
|
229
|
+
|
|
230
|
+
# Are we using native Ollama tool calling, otherwise we're doing manual tool calling
|
|
231
|
+
# We allow the user to decide if they want to use Ollama's tool calling
|
|
232
|
+
# or for tool calling to be handled manually through text messages
|
|
233
|
+
# Default is True = Ollama's tool calling
|
|
234
|
+
self._native_tool_calls = validate_parameter(params, "native_tool_calls", bool, False, True, None, None)
|
|
235
|
+
|
|
236
|
+
if not self._native_tool_calls:
|
|
237
|
+
# Load defaults
|
|
238
|
+
self._manual_tool_call_instruction = validate_parameter(
|
|
239
|
+
params, "manual_tool_call_instruction", str, False, self.TOOL_CALL_MANUAL_INSTRUCTION, None, None
|
|
240
|
+
)
|
|
241
|
+
self._manual_tool_call_step1 = validate_parameter(
|
|
242
|
+
params, "manual_tool_call_step1", str, False, self.TOOL_CALL_MANUAL_STEP1, None, None
|
|
243
|
+
)
|
|
244
|
+
self._manual_tool_call_step2 = validate_parameter(
|
|
245
|
+
params, "manual_tool_call_step2", str, False, self.TOOL_CALL_MANUAL_STEP2, None, None
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Convert AG2 messages to Ollama messages
|
|
249
|
+
ollama_messages = self.oai_messages_to_ollama_messages(
|
|
250
|
+
messages,
|
|
251
|
+
(
|
|
252
|
+
params["tools"]
|
|
253
|
+
if (not self._native_tool_calls and self._tools_in_conversation) and not self._should_hide_tools
|
|
254
|
+
else None
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Parse parameters to the Ollama API's parameters
|
|
259
|
+
ollama_params = self.parse_params(params)
|
|
260
|
+
|
|
261
|
+
ollama_params["messages"] = ollama_messages
|
|
262
|
+
|
|
263
|
+
# Token counts will be returned
|
|
264
|
+
prompt_tokens = 0
|
|
265
|
+
completion_tokens = 0
|
|
266
|
+
total_tokens = 0
|
|
267
|
+
|
|
268
|
+
ans = None
|
|
269
|
+
if "client_host" in params:
|
|
270
|
+
# Convert client_host to string from HttpUrl
|
|
271
|
+
client = Client(host=str(params["client_host"]))
|
|
272
|
+
response = client.chat(**ollama_params)
|
|
273
|
+
else:
|
|
274
|
+
response = ollama.chat(**ollama_params)
|
|
275
|
+
|
|
276
|
+
if ollama_params["stream"]:
|
|
277
|
+
# Read in the chunks as they stream, taking in tool_calls which may be across
|
|
278
|
+
# multiple chunks if more than one suggested
|
|
279
|
+
ans = ""
|
|
280
|
+
for chunk in response:
|
|
281
|
+
ans = ans + (chunk["message"]["content"] or "")
|
|
282
|
+
|
|
283
|
+
if "done_reason" in chunk:
|
|
284
|
+
prompt_tokens = chunk.get("prompt_eval_count", 0)
|
|
285
|
+
completion_tokens = chunk.get("eval_count", 0)
|
|
286
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
287
|
+
else:
|
|
288
|
+
# Non-streaming finished
|
|
289
|
+
ans: str = response["message"]["content"]
|
|
290
|
+
|
|
291
|
+
prompt_tokens = response.get("prompt_eval_count", 0)
|
|
292
|
+
completion_tokens = response.get("eval_count", 0)
|
|
293
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
294
|
+
|
|
295
|
+
if response is not None:
|
|
296
|
+
# Defaults
|
|
297
|
+
ollama_finish = "stop"
|
|
298
|
+
tool_calls = None
|
|
299
|
+
|
|
300
|
+
# Id and streaming text into response
|
|
301
|
+
if ollama_params["stream"]:
|
|
302
|
+
response_content = ans
|
|
303
|
+
response_id = chunk["created_at"]
|
|
304
|
+
else:
|
|
305
|
+
response_content = response["message"]["content"]
|
|
306
|
+
response_id = response["created_at"]
|
|
307
|
+
|
|
308
|
+
# Process tools in the response
|
|
309
|
+
if self._tools_in_conversation:
|
|
310
|
+
if self._native_tool_calls:
|
|
311
|
+
if not ollama_params["stream"]:
|
|
312
|
+
response_content = response["message"]["content"]
|
|
313
|
+
|
|
314
|
+
# Native tool calling
|
|
315
|
+
if "tool_calls" in response["message"]:
|
|
316
|
+
ollama_finish = "tool_calls"
|
|
317
|
+
tool_calls = []
|
|
318
|
+
random_id = random.randint(0, 10000)
|
|
319
|
+
for tool_call in response["message"]["tool_calls"]:
|
|
320
|
+
tool_calls.append(
|
|
321
|
+
ChatCompletionMessageToolCall(
|
|
322
|
+
id=f"ollama_func_{random_id}",
|
|
323
|
+
function={
|
|
324
|
+
"name": tool_call["function"]["name"],
|
|
325
|
+
"arguments": json.dumps(tool_call["function"]["arguments"]),
|
|
326
|
+
},
|
|
327
|
+
type="function",
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
random_id += 1
|
|
332
|
+
|
|
333
|
+
elif not self._native_tool_calls:
|
|
334
|
+
# Try to convert the response to a tool call object
|
|
335
|
+
response_toolcalls = response_to_tool_call(ans)
|
|
336
|
+
|
|
337
|
+
# If we can, then we've got tool call(s)
|
|
338
|
+
if response_toolcalls is not None:
|
|
339
|
+
ollama_finish = "tool_calls"
|
|
340
|
+
tool_calls = []
|
|
341
|
+
random_id = random.randint(0, 10000)
|
|
342
|
+
|
|
343
|
+
for json_function in response_toolcalls:
|
|
344
|
+
tool_calls.append(
|
|
345
|
+
ChatCompletionMessageToolCall(
|
|
346
|
+
id=f"ollama_manual_func_{random_id}",
|
|
347
|
+
function={
|
|
348
|
+
"name": json_function["name"],
|
|
349
|
+
"arguments": (
|
|
350
|
+
json.dumps(json_function["arguments"])
|
|
351
|
+
if "arguments" in json_function
|
|
352
|
+
else "{}"
|
|
353
|
+
),
|
|
354
|
+
},
|
|
355
|
+
type="function",
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
random_id += 1
|
|
360
|
+
|
|
361
|
+
# Blank the message content
|
|
362
|
+
response_content = ""
|
|
363
|
+
|
|
364
|
+
if ollama_finish == "stop": # noqa: SIM102
|
|
365
|
+
# Not a tool call, so let's check if we need to process structured output
|
|
366
|
+
if self._response_format and response_content:
|
|
367
|
+
try:
|
|
368
|
+
parsed_response = self._convert_json_response(response_content)
|
|
369
|
+
response_content = _format_json_response(parsed_response, response_content)
|
|
370
|
+
except ValueError as e:
|
|
371
|
+
response_content = str(e)
|
|
372
|
+
else:
|
|
373
|
+
raise RuntimeError("Failed to get response from Ollama.")
|
|
374
|
+
|
|
375
|
+
# Convert response to AG2 response
|
|
376
|
+
message = ChatCompletionMessage(
|
|
377
|
+
role="assistant",
|
|
378
|
+
content=response_content,
|
|
379
|
+
function_call=None,
|
|
380
|
+
tool_calls=tool_calls,
|
|
381
|
+
)
|
|
382
|
+
choices = [Choice(finish_reason=ollama_finish, index=0, message=message)]
|
|
383
|
+
|
|
384
|
+
response_oai = ChatCompletion(
|
|
385
|
+
id=response_id,
|
|
386
|
+
model=ollama_params["model"],
|
|
387
|
+
created=int(time.time()),
|
|
388
|
+
object="chat.completion",
|
|
389
|
+
choices=choices,
|
|
390
|
+
usage=CompletionUsage(
|
|
391
|
+
prompt_tokens=prompt_tokens,
|
|
392
|
+
completion_tokens=completion_tokens,
|
|
393
|
+
total_tokens=total_tokens,
|
|
394
|
+
),
|
|
395
|
+
cost=0, # Local models, FREE!
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return response_oai
|
|
399
|
+
|
|
400
|
+
def oai_messages_to_ollama_messages(self, messages: list[dict[str, Any]], tools: list) -> list[dict[str, Any]]:
|
|
401
|
+
"""Convert messages from OAI format to Ollama's format.
|
|
402
|
+
We correct for any specific role orders and types, and convert tools to messages (as Ollama can't use tool messages)
|
|
403
|
+
"""
|
|
404
|
+
ollama_messages = copy.deepcopy(messages)
|
|
405
|
+
|
|
406
|
+
# Remove the name field
|
|
407
|
+
for message in ollama_messages:
|
|
408
|
+
if "name" in message:
|
|
409
|
+
message.pop("name", None)
|
|
410
|
+
|
|
411
|
+
# Having a 'system' message on the end does not work well with Ollama, so we change it to 'user'
|
|
412
|
+
# 'system' messages on the end are typical of the summarisation message: summary_method="reflection_with_llm"
|
|
413
|
+
if len(ollama_messages) > 1 and ollama_messages[-1]["role"] == "system":
|
|
414
|
+
ollama_messages[-1]["role"] = "user"
|
|
415
|
+
|
|
416
|
+
# Process messages for tool calling manually
|
|
417
|
+
if tools is not None and not self._native_tool_calls:
|
|
418
|
+
# 1. We need to append instructions to the starting system message on function calling
|
|
419
|
+
# 2. If we have not yet called tools we append "step 1 instruction" to the latest user message
|
|
420
|
+
# 3. If we have already called tools we append "step 2 instruction" to the latest user message
|
|
421
|
+
|
|
422
|
+
have_tool_calls = False
|
|
423
|
+
have_tool_results = False
|
|
424
|
+
last_tool_result_index = -1
|
|
425
|
+
|
|
426
|
+
for i, message in enumerate(ollama_messages):
|
|
427
|
+
if "tool_calls" in message:
|
|
428
|
+
have_tool_calls = True
|
|
429
|
+
if "tool_call_id" in message:
|
|
430
|
+
have_tool_results = True
|
|
431
|
+
last_tool_result_index = i
|
|
432
|
+
|
|
433
|
+
tool_result_is_last_msg = have_tool_results and last_tool_result_index == len(ollama_messages) - 1
|
|
434
|
+
|
|
435
|
+
if ollama_messages[0]["role"] == "system":
|
|
436
|
+
manual_instruction = self._manual_tool_call_instruction
|
|
437
|
+
|
|
438
|
+
# Build a string of the functions available
|
|
439
|
+
functions_string = ""
|
|
440
|
+
for function in tools:
|
|
441
|
+
functions_string += f"""\n{function}\n"""
|
|
442
|
+
|
|
443
|
+
# Replace single quotes with double questions - Not sure why this helps the LLM perform
|
|
444
|
+
# better, but it seems to. Monitor and remove if not necessary.
|
|
445
|
+
functions_string = functions_string.replace("'", '"')
|
|
446
|
+
|
|
447
|
+
manual_instruction = manual_instruction.replace("[FUNCTIONS_LIST]", functions_string)
|
|
448
|
+
|
|
449
|
+
# Update the system message with the instructions and functions
|
|
450
|
+
ollama_messages[0]["content"] = ollama_messages[0]["content"] + manual_instruction.rstrip()
|
|
451
|
+
|
|
452
|
+
# If we are still in the function calling or evaluating process, append the steps instruction
|
|
453
|
+
if (not have_tool_calls or tool_result_is_last_msg) and ollama_messages[0]["role"] == "system":
|
|
454
|
+
# NOTE: we require a system message to exist for the manual steps texts
|
|
455
|
+
# Append the manual step instructions
|
|
456
|
+
content_to_append = (
|
|
457
|
+
self._manual_tool_call_step1 if not have_tool_results else self._manual_tool_call_step2
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if content_to_append != "":
|
|
461
|
+
# Append the relevant tool call instruction to the latest user message
|
|
462
|
+
if ollama_messages[-1]["role"] == "user":
|
|
463
|
+
ollama_messages[-1]["content"] = ollama_messages[-1]["content"] + content_to_append
|
|
464
|
+
else:
|
|
465
|
+
ollama_messages.append({"role": "user", "content": content_to_append})
|
|
466
|
+
|
|
467
|
+
# Convert tool call and tool result messages to normal text messages for Ollama
|
|
468
|
+
for i, message in enumerate(ollama_messages):
|
|
469
|
+
if "tool_calls" in message:
|
|
470
|
+
# Recommended tool calls
|
|
471
|
+
content = "Run the following function(s):"
|
|
472
|
+
for tool_call in message["tool_calls"]:
|
|
473
|
+
content = content + "\n" + str(tool_call)
|
|
474
|
+
ollama_messages[i] = {"role": "assistant", "content": content}
|
|
475
|
+
if "tool_call_id" in message:
|
|
476
|
+
# Executed tool results
|
|
477
|
+
message["result"] = message["content"]
|
|
478
|
+
del message["content"]
|
|
479
|
+
del message["role"]
|
|
480
|
+
content = "The following function was run: " + str(message)
|
|
481
|
+
ollama_messages[i] = {"role": "user", "content": content}
|
|
482
|
+
|
|
483
|
+
# As we are changing messages, let's merge if they have two user messages on the end and the last one is tool call step instructions
|
|
484
|
+
if (
|
|
485
|
+
len(ollama_messages) >= 2
|
|
486
|
+
and not self._native_tool_calls
|
|
487
|
+
and ollama_messages[-2]["role"] == "user"
|
|
488
|
+
and ollama_messages[-1]["role"] == "user"
|
|
489
|
+
and (
|
|
490
|
+
ollama_messages[-1]["content"] == self._manual_tool_call_step1
|
|
491
|
+
or ollama_messages[-1]["content"] == self._manual_tool_call_step2
|
|
492
|
+
)
|
|
493
|
+
):
|
|
494
|
+
ollama_messages[-2]["content"] = ollama_messages[-2]["content"] + ollama_messages[-1]["content"]
|
|
495
|
+
del ollama_messages[-1]
|
|
496
|
+
|
|
497
|
+
# Ensure the last message is a user / system message, if not, add a user message
|
|
498
|
+
if ollama_messages[-1]["role"] != "user" and ollama_messages[-1]["role"] != "system":
|
|
499
|
+
ollama_messages.append({"role": "user", "content": "Please continue."})
|
|
500
|
+
|
|
501
|
+
return ollama_messages
|
|
502
|
+
|
|
503
|
+
def _convert_json_response(self, response: str) -> Any:
|
|
504
|
+
"""Extract and validate JSON response from the output for structured outputs.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
response (str): The response from the API.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Any: The parsed JSON response.
|
|
511
|
+
"""
|
|
512
|
+
if not self._response_format:
|
|
513
|
+
return response
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
# Parse JSON and validate against the Pydantic model if Pydantic model was provided
|
|
517
|
+
if isinstance(self._response_format, dict):
|
|
518
|
+
return response
|
|
519
|
+
else:
|
|
520
|
+
return self._response_format.model_validate_json(response)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
raise ValueError(f"Failed to parse response as valid JSON matching the schema for Structured Output: {e!s}")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _format_json_response(response: Any, original_answer: str) -> str:
|
|
526
|
+
"""Formats the JSON response for structured outputs using the format method if it exists."""
|
|
527
|
+
return response.format() if isinstance(response, FormatterProtocol) else original_answer
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@require_optional_import("fix_busted_json", "ollama")
|
|
531
|
+
def response_to_tool_call(response_string: str) -> Any:
|
|
532
|
+
"""Attempts to convert the response to an object, aimed to align with function format `[{},{}]`"""
|
|
533
|
+
# We try and detect the list[dict[str, Any]] format:
|
|
534
|
+
# Pattern 1 is [{},{}]
|
|
535
|
+
# Pattern 2 is {} (without the [], so could be a single function call)
|
|
536
|
+
patterns = [r"\[[\s\S]*?\]", r"\{[\s\S]*\}"]
|
|
537
|
+
|
|
538
|
+
for i, pattern in enumerate(patterns):
|
|
539
|
+
# Search for the pattern in the input string
|
|
540
|
+
matches = re.findall(pattern, response_string.strip())
|
|
541
|
+
|
|
542
|
+
for match in matches:
|
|
543
|
+
# It has matched, extract it and load it
|
|
544
|
+
json_str = match.strip()
|
|
545
|
+
data_object = None
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
# Attempt to convert it as is
|
|
549
|
+
data_object = json.loads(json_str)
|
|
550
|
+
except Exception:
|
|
551
|
+
try:
|
|
552
|
+
# If that fails, attempt to repair it
|
|
553
|
+
|
|
554
|
+
if i == 0:
|
|
555
|
+
# Enclose to a JSON object for repairing, which is restored upon fix
|
|
556
|
+
fixed_json = repair_json("{'temp':" + json_str + "}")
|
|
557
|
+
data_object = json.loads(fixed_json)
|
|
558
|
+
data_object = data_object["temp"]
|
|
559
|
+
else:
|
|
560
|
+
fixed_json = repair_json(json_str)
|
|
561
|
+
data_object = json.loads(fixed_json)
|
|
562
|
+
except json.JSONDecodeError as e:
|
|
563
|
+
if e.msg == "Invalid \\escape":
|
|
564
|
+
# Handle Mistral/Mixtral trying to escape underlines with \\
|
|
565
|
+
try:
|
|
566
|
+
json_str = json_str.replace("\\_", "_")
|
|
567
|
+
if i == 0:
|
|
568
|
+
fixed_json = repair_json("{'temp':" + json_str + "}")
|
|
569
|
+
data_object = json.loads(fixed_json)
|
|
570
|
+
data_object = data_object["temp"]
|
|
571
|
+
else:
|
|
572
|
+
fixed_json = repair_json("{'temp':" + json_str + "}")
|
|
573
|
+
data_object = json.loads(fixed_json)
|
|
574
|
+
except Exception:
|
|
575
|
+
pass
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
if data_object is not None:
|
|
580
|
+
data_object = _object_to_tool_call(data_object)
|
|
581
|
+
|
|
582
|
+
if data_object is not None:
|
|
583
|
+
return data_object
|
|
584
|
+
|
|
585
|
+
# There's no tool call in the response
|
|
586
|
+
return None
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _object_to_tool_call(data_object: Any) -> list[dict[str, Any]]:
|
|
590
|
+
"""Attempts to convert an object to a valid tool call object List[Dict] and returns it, if it can, otherwise None"""
|
|
591
|
+
# If it's a dictionary and not a list then wrap in a list
|
|
592
|
+
if isinstance(data_object, dict):
|
|
593
|
+
data_object = [data_object]
|
|
594
|
+
|
|
595
|
+
# Validate that the data is a list of dictionaries
|
|
596
|
+
if isinstance(data_object, list) and all(isinstance(item, dict) for item in data_object):
|
|
597
|
+
# Perfect format, a list of dictionaries
|
|
598
|
+
|
|
599
|
+
# Check that each dictionary has at least 'name', optionally 'arguments' and no other keys
|
|
600
|
+
is_invalid = False
|
|
601
|
+
for item in data_object:
|
|
602
|
+
if not is_valid_tool_call_item(item):
|
|
603
|
+
is_invalid = True
|
|
604
|
+
break
|
|
605
|
+
|
|
606
|
+
# All passed, name and (optionally) arguments exist for all entries.
|
|
607
|
+
if not is_invalid:
|
|
608
|
+
return data_object
|
|
609
|
+
elif isinstance(data_object, list):
|
|
610
|
+
# If it's a list but the items are not dictionaries, check if they are strings that can be converted to dictionaries
|
|
611
|
+
data_copy = data_object.copy()
|
|
612
|
+
is_invalid = False
|
|
613
|
+
for i, item in enumerate(data_copy):
|
|
614
|
+
try:
|
|
615
|
+
new_item = eval(item)
|
|
616
|
+
if isinstance(new_item, dict):
|
|
617
|
+
if is_valid_tool_call_item(new_item):
|
|
618
|
+
data_object[i] = new_item
|
|
619
|
+
else:
|
|
620
|
+
is_invalid = True
|
|
621
|
+
break
|
|
622
|
+
else:
|
|
623
|
+
is_invalid = True
|
|
624
|
+
break
|
|
625
|
+
except Exception:
|
|
626
|
+
is_invalid = True
|
|
627
|
+
break
|
|
628
|
+
|
|
629
|
+
if not is_invalid:
|
|
630
|
+
return data_object
|
|
631
|
+
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def is_valid_tool_call_item(call_item: dict) -> bool:
|
|
636
|
+
"""Check that a dictionary item has at least 'name', optionally 'arguments' and no other keys to match a tool call JSON"""
|
|
637
|
+
if "name" not in call_item or not isinstance(call_item["name"], str):
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
if set(call_item.keys()) - {"name", "arguments"}: # noqa: SIM103
|
|
641
|
+
return False
|
|
642
|
+
|
|
643
|
+
return True
|