Repository: deepset-ai/haystack

Issue:
`ComponentMeta.__call__` ignores keyword only parameters
**Describe the bug**
`ComponentMeta.__call__` handles the creation of `InputSocket`s for the component's inputs when the latter has not explicitly called `_Component.set_input_types()`. This [logic](https://github.com/deepset-ai/haystack/blob/00e1dd6eb86a67bfb56c24e2558bf3ac5974f353/haystack/core/component/component.py#L142), however, doesn't appear to correctly handle keyword-only parameters. If a component has a keyword-only parameter in its `run` method, it gets ignored, which leads to i/o mismatch during pipeline creation.

**Error message**
`ValueError: Component XXX is not expecting any input value called YYYY.` where YYYY is a kw-only parameter.

**Expected behavior**
It isn't immediately clear if this is intentional behaviour, but the use of `inspect.Parameter.POSITIONAL_OR_KEYWORD` seems to indicate that it's unintentional. The above enum variant, however, doesn't act as a mask of both `inspect.Parameter.POSITIONAL_ONLY` and `inspect.Parameter.KEYWORD_ONLY`.

**Additional context**
One possible solution: Replace the conditional with the following
```
if run_signature.parameters[param].kind in (
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.KEYWORD_ONLY,
            ):
```

**To Reproduce**
Add a component with a kw-only `run()` parameter to a pipeline.

**System:**
 - Haystack version (commit or version number): https://github.com/deepset-ai/haystack/commit/bb2b1a20f8ce6c0144ce688764bd5834c1291001


Discussion:

Resolving patch:
diff --git a/haystack/core/component/component.py b/haystack/core/component/component.py
--- a/haystack/core/component/component.py
+++ b/haystack/core/component/component.py
@@ -139,7 +139,10 @@ def __call__(cls, *args, **kwargs):
             instance.__canals_input__ = {}
         run_signature = inspect.signature(getattr(cls, "run"))
         for param in list(run_signature.parameters)[1:]:  # First is 'self' and it doesn't matter.
-            if run_signature.parameters[param].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:  # ignore `**kwargs`
+            if run_signature.parameters[param].kind not in (
+                inspect.Parameter.VAR_POSITIONAL,
+                inspect.Parameter.VAR_KEYWORD,
+            ):  # ignore variable args
                 socket_kwargs = {"name": param, "type": run_signature.parameters[param].annotation}
                 if run_signature.parameters[param].default != inspect.Parameter.empty:
                     socket_kwargs["default_value"] = run_signature.parameters[param].default


Test patch:
diff --git a/test/core/component/test_component.py b/test/core/component/test_component.py
--- a/test/core/component/test_component.py
+++ b/test/core/component/test_component.py
@@ -176,3 +176,17 @@ def run(self, value: int = 42):
     comp = MockComponent()
     assert comp.__canals_input__["value"].default_value == 42
     assert not comp.__canals_input__["value"].is_mandatory
+
+
+def test_keyword_only_args():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, *, arg: int):
+            return {"value": arg}
+
+    comp = MockComponent()
+    component_inputs = {name: {"type": socket.type} for name, socket in comp.__canals_input__.items()}
+    assert component_inputs == {"arg": {"type": int}}