|
12 | 12 |
|
13 | 13 | from fastapi import HTTPException |
14 | 14 | from lfx.base.models.model_remediation import cached_overrides, find_remediation, remember, restore_overrides |
| 15 | +from lfx.base.models.provider_registry import get_registry_snapshot |
15 | 16 | from lfx.graph.flow_builder.flow import flow_to_spec_summary |
16 | 17 | from lfx.log.logger import logger |
17 | 18 | from lfx.mcp.flow_builder_tools import ( |
|
23 | 24 | set_propose_existing_edits, |
24 | 25 | ) |
25 | 26 | from lfx.mcp.tool_cache import reset_tool_cache |
| 27 | +from lfx.services.model_provider_policy import ModelProviderPolicyPurpose, aresolve_model_provider_policy |
26 | 28 |
|
27 | 29 | from langflow.agentic.helpers.code_extraction import extract_component_code, extract_flow_json |
28 | 30 | from langflow.agentic.helpers.code_security import scan_code_security |
@@ -718,29 +720,79 @@ def _complete(data: dict) -> str: |
718 | 720 | f"{current_input}" |
719 | 721 | ) |
720 | 722 |
|
721 | | - # Tell the agent which language model(s) it can safely put on any Agent |
722 | | - # it builds — building an Agent without a model makes the run fail with |
723 | | - # "No model selected". The PREFERRED one is the model the assistant |
| 723 | + # Tell the agent which language model(s) it can safely put on any Agent it |
| 724 | + # builds — building an Agent without a model makes the run fail with |
| 725 | + # "No model selected". |
| 726 | + # Resolve one CONFIGURE snapshot before exposing names or binding the |
| 727 | + # classifier's requested model; this keeps the assistant from reintroducing |
| 728 | + # a provider hidden by governance through prompt context or deterministic |
| 729 | + # run-model injection. |
| 730 | + # The PREFERRED one is the model the assistant |
724 | 731 | # itself runs with (key guaranteed). We also list every provider whose |
725 | 732 | # API key is configured (provider-agnostic, detected from the env-built |
726 | 733 | # global variables — NO OpenAI bias). Omitted (input byte-identical to |
727 | 734 | # before) only when neither is available. |
728 | 735 | from langflow.agentic.services.flow_preparation import available_model_providers |
729 | 736 |
|
| 737 | + _available_provider_names = available_model_providers(global_variables) |
| 738 | + _registered_provider_names = sorted( |
| 739 | + descriptor.name for descriptor in get_registry_snapshot().descriptors_by_id.values() |
| 740 | + ) |
| 741 | + _policy_candidates = list( |
| 742 | + dict.fromkeys( |
| 743 | + [ |
| 744 | + *_registered_provider_names, |
| 745 | + *_available_provider_names, |
| 746 | + *([provider] if provider else []), |
| 747 | + *([intent_result.requested_provider] if intent_result.requested_provider else []), |
| 748 | + ] |
| 749 | + ) |
| 750 | + ) |
| 751 | + try: |
| 752 | + _provider_policy = await aresolve_model_provider_policy( |
| 753 | + user_id=user_id, |
| 754 | + providers=_policy_candidates, |
| 755 | + purpose=ModelProviderPolicyPurpose.CONFIGURE, |
| 756 | + ) |
| 757 | + except BaseException: |
| 758 | + # The current canvas was already seeded above, but the main request |
| 759 | + # try/finally has not started yet. A fail-closed policy error or |
| 760 | + # cancellation must not leak that ContextVar state to the next request. |
| 761 | + reset_working_flow() |
| 762 | + raise |
| 763 | + _requested_provider = intent_result.requested_provider |
| 764 | + _requested_provider_allowed = not _requested_provider or _provider_policy.allows(_requested_provider) |
| 765 | + _allowed_configuration_providers = _provider_policy.filter(_registered_provider_names) |
| 766 | + _catalog_is_restricted = any(not _provider_policy.allows(name) for name in _policy_candidates) |
| 767 | + |
730 | 768 | _model_parts: list[str] = [] |
731 | | - if provider and model_name: |
| 769 | + if provider and model_name and _provider_policy.allows(provider): |
732 | 770 | _model_parts.append(f"preferred: provider={provider!r}, name={model_name!r}") |
733 | | - _avail = available_model_providers(global_variables) |
| 771 | + _avail = _provider_policy.filter(_available_provider_names) |
734 | 772 | if _avail: |
735 | 773 | _model_parts.append("providers with credentials configured: " + ", ".join(_avail)) |
736 | 774 | if _model_parts: |
737 | 775 | current_input = ( |
738 | 776 | f"[Available language models — these are a DEFAULT only. If the user explicitly named a " |
739 | | - f"model, set EXACTLY that model (verbatim) and IGNORE this block. ONLY when the user did " |
740 | | - f"NOT name a model, configure an Agent's `model` field with the one marked `preferred` " |
741 | | - f"(else any listed provider) so the flow can run: " |
| 777 | + f"model and no Model provider policy notice rejects it, set EXACTLY that model and IGNORE " |
| 778 | + f"this block. ONLY when the user did NOT name a model, configure an Agent's `model` field " |
| 779 | + f"with the one marked `preferred` (else any listed provider) so the flow can run: " |
742 | 780 | f"{'; '.join(_model_parts)}]\n\n{current_input}" |
743 | 781 | ) |
| 782 | + if _catalog_is_restricted or not _requested_provider_allowed: |
| 783 | + allowed_notice = ( |
| 784 | + "Configure only these providers: " + ", ".join(_allowed_configuration_providers) + ". " |
| 785 | + if _allowed_configuration_providers |
| 786 | + else "No model providers are available for configuration. " |
| 787 | + ) |
| 788 | + requested_notice = ( |
| 789 | + "The explicitly requested provider is unavailable. " if not _requested_provider_allowed else "" |
| 790 | + ) |
| 791 | + current_input = ( |
| 792 | + f"[Model provider policy: {requested_notice}{allowed_notice}" |
| 793 | + "Do not discover, configure, or run any other provider.]\n\n" |
| 794 | + f"{current_input}" |
| 795 | + ) |
744 | 796 |
|
745 | 797 | # Headless callers (MCP) have no review UI, so steer the agent away from a |
746 | 798 | # "proposed/pending approval" narration the user can never act on (#13641). |
@@ -829,16 +881,22 @@ async def check_cancelled() -> bool: |
829 | 881 | set_current_user_id(user_id) |
830 | 882 | # The generate_component tool re-runs the component-gen LLM flow |
831 | 883 | # mid-loop and needs the same provider/model the request used. |
832 | | - set_agent_run_model(provider, model_name, api_key_var) |
| 884 | + set_agent_run_model( |
| 885 | + provider, |
| 886 | + model_name, |
| 887 | + api_key_var, |
| 888 | + allow_configuration=bool(provider and _provider_policy.allows(provider)), |
| 889 | + ) |
833 | 890 | set_agent_run_iterations(_iterations_from_globals(global_variables)) |
834 | 891 | # If the user EXPLICITLY named a model (e.g. "use the OpenAI gpt-5.4 |
835 | 892 | # model"), bind it so the run-time injector ENFORCES it on the Agent — |
836 | 893 | # the canvas must show exactly what the user asked for, never the |
837 | 894 | # assistant's own runtime model. Same-provider runs reuse the verified |
838 | 895 | # api_key_var; a different provider falls back to its default var. |
839 | | - _req_provider = intent_result.requested_provider |
| 896 | + _req_provider = intent_result.requested_provider if _requested_provider_allowed else None |
840 | 897 | _req_api_key_var = api_key_var if (_req_provider and provider and _req_provider == provider) else None |
841 | | - set_requested_agent_model(_req_provider, intent_result.requested_model, _req_api_key_var) |
| 898 | + _req_model = intent_result.requested_model if _req_provider else None |
| 899 | + set_requested_agent_model(_req_provider, _req_model, _req_api_key_var) |
842 | 900 |
|
843 | 901 | # max_retries=0 means 1 attempt (no retries), matching non-streaming semantics |
844 | 902 | total_attempts = max_retries + 1 |
|
0 commit comments