Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions libs/langchain_v1/langchain/agents/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,18 +417,15 @@ def _handle_structured_output_error(
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
if isinstance(handle_errors, str):
return True, handle_errors
if isinstance(handle_errors, type) and issubclass(handle_errors, Exception):
if isinstance(exception, handle_errors):
if isinstance(handle_errors, type):
if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
return False, ""
if isinstance(handle_errors, tuple):
if any(isinstance(exception, exc_type) for exc_type in handle_errors):
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
return False, ""
if callable(handle_errors):
# type narrowing not working appropriately w/ callable check, can fix later
return True, handle_errors(exception) # type: ignore[return-value,call-arg]
return False, ""
return True, handle_errors(exception)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type narrowing now works because handle_errors cannot be a type anymore at this stage.
Note that type is callable.



def _chain_tool_call_wrappers(
Expand Down Expand Up @@ -547,7 +544,7 @@ def create_agent(
*,
system_prompt: str | SystemMessage | None = None,
middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),
response_format: ResponseFormat[ResponseT] | type[ResponseT] | None = None,
response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to test_json_schema, create_agent's response_format can be a dict.
Which means it has to be supported in AutoStrategy/ToolStrategy/ProviderStrategy/_SchemaSpec

state_schema: type[AgentState[ResponseT]] | None = None,
context_schema: type[ContextT] | None = None,
checkpointer: Checkpointer | None = None,
Expand Down
20 changes: 8 additions & 12 deletions libs/langchain_v1/langchain/agents/middleware/_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

try: # pragma: no cover - optional dependency on POSIX platforms
import resource

_HAS_RESOURCE = True
except ImportError: # pragma: no cover - non-POSIX systems
resource = None # type: ignore[assignment]
_HAS_RESOURCE = False


SHELL_TEMP_PREFIX = "langchain-shell-"
Expand Down Expand Up @@ -119,7 +121,7 @@ def __post_init__(self) -> None:
self._limits_requested = any(
value is not None for value in (self.cpu_time_seconds, self.memory_bytes)
)
if self._limits_requested and resource is None:
if self._limits_requested and not _HAS_RESOURCE:
msg = (
"HostExecutionPolicy cpu/memory limits require the Python 'resource' module. "
"Either remove the limits or run on a POSIX platform."
Expand Down Expand Up @@ -163,11 +165,9 @@ def _configure() -> None: # pragma: no cover - depends on OS
def _apply_post_spawn_limits(self, process: subprocess.Popen[str]) -> None:
if not self._limits_requested or not self._can_use_prlimit():
return
if resource is None: # pragma: no cover - defensive
if not _HAS_RESOURCE: # pragma: no cover - defensive
return
pid = process.pid
if pid is None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess.Popen.pid is never None.
Is it OK ?

return
try:
prlimit = typing.cast("typing.Any", resource).prlimit
if self.cpu_time_seconds is not None:
Expand All @@ -184,11 +184,7 @@ def _apply_post_spawn_limits(self, process: subprocess.Popen[str]) -> None:

@staticmethod
def _can_use_prlimit() -> bool:
return (
resource is not None
and hasattr(resource, "prlimit")
and sys.platform.startswith("linux")
)
return _HAS_RESOURCE and hasattr(resource, "prlimit") and sys.platform.startswith("linux")


@dataclass
Expand Down Expand Up @@ -251,9 +247,9 @@ def _determine_platform(self) -> str:
return self.platform
if sys.platform.startswith("linux"):
return "linux"
if sys.platform == "darwin":
if sys.platform == "darwin": # type: ignore[unreachable, unused-ignore]
return "macos"
msg = (
msg = ( # type: ignore[unreachable, unused-ignore]
"Codex sandbox policy could not determine a supported platform; "
"set 'platform' explicitly."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def apply_strategy(
return _apply_hash_strategy(content, matches)
if strategy == "block":
raise PIIDetectionError(matches[0]["type"], matches)
msg = f"Unknown redaction strategy: {strategy}"
msg = f"Unknown redaction strategy: {strategy}" # type: ignore[unreachable]
raise ValueError(msg)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ def _normalize_env(env: Mapping[str, Any] | None) -> dict[str, str] | None:
normalized: dict[str, str] = {}
for key, value in env.items():
if not isinstance(key, str):
msg = "Environment variable names must be strings."
msg = "Environment variable names must be strings." # type: ignore[unreachable]
raise TypeError(msg)
normalized[key] = str(value)
return normalized
Expand Down
4 changes: 2 additions & 2 deletions libs/langchain_v1/langchain/agents/middleware/tool_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,14 @@ def __init__(

# Handle backwards compatibility for deprecated on_failure values
if on_failure == "raise": # type: ignore[comparison-overlap]
msg = (
msg = ( # type: ignore[unreachable]
"on_failure='raise' is deprecated and will be removed in a future version. "
"Use on_failure='error' instead."
)
warnings.warn(msg, DeprecationWarning, stacklevel=2)
on_failure = "error"
elif on_failure == "return_message": # type: ignore[comparison-overlap]
msg = (
msg = ( # type: ignore[unreachable]
"on_failure='return_message' is deprecated and will be removed "
"in a future version. Use on_failure='continue' instead."
)
Expand Down
2 changes: 1 addition & 1 deletion libs/langchain_v1/langchain/agents/middleware/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def override(self, **overrides: Unpack[_ModelRequestOverrides]) -> ModelRequest:
raise ValueError(msg)

if "system_prompt" in overrides:
system_prompt = cast("str", overrides.pop("system_prompt")) # type: ignore[typeddict-item]
system_prompt = cast("str | None", overrides.pop("system_prompt")) # type: ignore[typeddict-item]
if system_prompt is None:
overrides["system_message"] = None
else:
Expand Down
38 changes: 18 additions & 20 deletions libs/langchain_v1/langchain/agents/structured_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _parse_with_schema(
class _SchemaSpec(Generic[SchemaT]):
"""Describes a structured output schema."""

schema: type[SchemaT]
schema: type[SchemaT] | dict[str, Any]
"""The schema for the response, can be a Pydantic model, `dataclass`, `TypedDict`,
or JSON schema dict."""

Expand Down Expand Up @@ -134,7 +134,7 @@ class _SchemaSpec(Generic[SchemaT]):

def __init__(
self,
schema: type[SchemaT],
schema: type[SchemaT] | dict[str, Any],
*,
name: str | None = None,
description: str | None = None,
Expand Down Expand Up @@ -257,15 +257,15 @@ def _iter_variants(schema: Any) -> Iterable[Any]:
class ProviderStrategy(Generic[SchemaT]):
"""Use the model provider's native structured output method."""

schema: type[SchemaT]
schema: type[SchemaT] | dict[str, Any]
"""Schema for native mode."""

schema_spec: _SchemaSpec[SchemaT]
"""Schema spec for native mode."""

def __init__(
self,
schema: type[SchemaT],
schema: type[SchemaT] | dict[str, Any],
*,
strict: bool | None = None,
) -> None:
Expand Down Expand Up @@ -309,7 +309,7 @@ class OutputToolBinding(Generic[SchemaT]):
and the corresponding tool implementation used by the tools strategy.
"""

schema: type[SchemaT]
schema: type[SchemaT] | dict[str, Any]
"""The original schema provided for structured output
(Pydantic model, dataclass, TypedDict, or JSON schema dict)."""

Expand Down Expand Up @@ -363,7 +363,7 @@ class ProviderStrategyBinding(Generic[SchemaT]):
its type classification, and parsing logic for provider-enforced JSON.
"""

schema: type[SchemaT]
schema: type[SchemaT] | dict[str, Any]
"""The original schema provided for structured output
(Pydantic model, `dataclass`, `TypedDict`, or JSON schema dict)."""

Expand Down Expand Up @@ -426,29 +426,27 @@ def _extract_text_content_from_message(message: AIMessage) -> str:
content = message.content
if isinstance(content, str):
return content
if isinstance(content, list):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseMessage.content is either a str or a list.
So there should be no need to have a fallback.

parts: list[str] = []
for c in content:
if isinstance(c, dict):
if c.get("type") == "text" and "text" in c:
parts.append(str(c["text"]))
elif "content" in c and isinstance(c["content"], str):
parts.append(c["content"])
else:
parts.append(str(c))
return "".join(parts)
return str(content)
parts: list[str] = []
for c in content:
if isinstance(c, dict):
if c.get("type") == "text" and "text" in c:
parts.append(str(c["text"]))
elif "content" in c and isinstance(c["content"], str):
parts.append(c["content"])
else:
parts.append(str(c))
return "".join(parts)


class AutoStrategy(Generic[SchemaT]):
"""Automatically select the best strategy for structured output."""

schema: type[SchemaT]
schema: type[SchemaT] | dict[str, Any]
"""Schema for automatic mode."""

def __init__(
self,
schema: type[SchemaT],
schema: type[SchemaT] | dict[str, Any],
) -> None:
"""Initialize AutoStrategy with schema."""
self.schema = schema
Expand Down
1 change: 1 addition & 0 deletions libs/langchain_v1/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ line-length = 100
strict = true
ignore_missing_imports = true
enable_error_code = "deprecated"
warn_unreachable = true
exclude = ["tests/unit_tests/agents/*"]

# TODO: activate for 'strict' checking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_host_policy_validations() -> None:


def test_host_policy_requires_resource_for_limits(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(_execution, "resource", None, raising=False)
monkeypatch.setattr(_execution, "_HAS_RESOURCE", False, raising=False)
with pytest.raises(RuntimeError):
HostExecutionPolicy(cpu_time_seconds=1)

Expand Down