Repository: griptape-ai/griptape

Issue:
Type Hints Thrown Away By Decorator
![Image](https://github.com/user-attachments/assets/05f87fac-ff43-4d3e-be07-100756053037)

Appears to be an issue with anything wrapped by `@observable`.

Discussion:

Solution patch:
diff --git a/docs/examples/src/amazon_dynamodb_sessions_1.py b/docs/examples/src/amazon_dynamodb_sessions_1.py
index a5f9ee8b..f145caab 100644
--- a/docs/examples/src/amazon_dynamodb_sessions_1.py
+++ b/docs/examples/src/amazon_dynamodb_sessions_1.py
@@ -29,4 +29,4 @@ structure = Agent(
     )
 )
 
-print(structure.run(user_input).output_task.output.value)
+print(structure.run(user_input).output.value)
diff --git a/griptape/common/decorators.py b/griptape/common/decorators.py
index f1790b56..4a606522 100644
--- a/griptape/common/decorators.py
+++ b/griptape/common/decorators.py
@@ -1,15 +1,32 @@
 from __future__ import annotations
 
-from typing import Any, Callable, TypeVar, cast
+import functools
+from typing import Any, Callable, Optional, TypeVar, cast, overload
 
 import wrapt
+from typing_extensions import ParamSpec
 
 T = TypeVar("T")
+P = ParamSpec("P")
 
 
-def observable(*dargs: Any, **dkwargs: Any) -> Callable:
+@overload
+def observable(wrapped: None = None, **dkwargs: Any) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
+
+
+@overload
+def observable(wrapped: Callable[P, T], **dkwargs: Any) -> Callable[P, T]: ...
+
+
+def observable(wrapped: Optional[Callable[P, T] | Any] = None, **dkwargs: Any) -> Any:
+    if wrapped is None:
+        return functools.partial(observable, **dkwargs)
+
+    if not callable(wrapped):
+        raise ValueError("Non-callable positional argument passed. Use kwargs to pass arguments to the decorator.")
+
     @wrapt.decorator
-    def decorator(wrapped: Callable[..., T], instance: Any, args: Any, kwargs: Any) -> T:
+    def wrapper(wrapped: Callable[P, T], instance: Any, args: Any, kwargs: Any) -> T:
         from griptape.common.observable import Observable
         from griptape.observability.observability import Observability
 
@@ -21,19 +38,10 @@ def observable(*dargs: Any, **dkwargs: Any) -> Callable:
                     instance=instance,
                     args=args,
                     kwargs=kwargs,
-                    decorator_args=dargs,
+                    decorator_args=(),
                     decorator_kwargs=dkwargs,
                 )
             ),
         )
 
-    # Check if it's being called as @observable or @observable(...)
-    if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:  # pyright: ignore[reportArgumentType]
-        # Case when decorator is used without arguments
-        func = dargs[0]
-        dargs = ()
-        dkwargs = {}
-        return decorator(func)  # pyright: ignore[reportCallIssue]
-    else:
-        # Case when decorator is used with arguments
-        return decorator
+    return wrapper(wrapped)  # pyright: ignore[reportCallIssue]
diff --git a/griptape/drivers/structure_run/local_structure_run_driver.py b/griptape/drivers/structure_run/local_structure_run_driver.py
index b2335e3c..c3119dee 100644
--- a/griptape/drivers/structure_run/local_structure_run_driver.py
+++ b/griptape/drivers/structure_run/local_structure_run_driver.py
@@ -5,10 +5,10 @@ from typing import TYPE_CHECKING, Callable
 
 from attrs import define, field
 
-from griptape.artifacts import BaseArtifact, InfoArtifact
 from griptape.drivers.structure_run.base_structure_run_driver import BaseStructureRunDriver
 
 if TYPE_CHECKING:
+    from griptape.artifacts import BaseArtifact
     from griptape.structures import Structure
 
 
@@ -25,7 +25,4 @@ class LocalStructureRunDriver(BaseStructureRunDriver):
             os.environ.clear()
             os.environ.update(old_env)
 
-        if structure.output_task.output is not None:
-            return structure.output_task.output
-        else:
-            return InfoArtifact("No output found in response")
+        return structure.output
diff --git a/griptape/utils/chat.py b/griptape/utils/chat.py
index bc2c0d9a..34934d05 100644
--- a/griptape/utils/chat.py
+++ b/griptape/utils/chat.py
@@ -86,9 +86,7 @@ class Chat:
                     self._call_handle_output(chunk.value, stream=True)
             else:
                 self._call_handle_output(self.processing_text)
-                self._call_handle_output(
-                    f"{self.response_prefix}{self.structure.run(question).output_task.output.to_text()}"
-                )
+                self._call_handle_output(f"{self.response_prefix}{self.structure.run(question).output.to_text()}")
 
         # Restore the original logger level
         logging.getLogger(Defaults.logging_config.logger_name).setLevel(old_logger_level)


Test patch:
diff --git a/tests/unit/common/test_observable.py b/tests/unit/common/test_observable.py
index c06be5bb..874c3824 100644
--- a/tests/unit/common/test_observable.py
+++ b/tests/unit/common/test_observable.py
@@ -71,7 +71,7 @@ class TestObservable:
     def test_observable_function_args(self, observe_spy):
         from griptape.common import observable
 
-        @observable("one", 2, {"th": "ree"}, a="b", b=6)
+        @observable(a="one", b=2, c={"th": "ree"}, d="b", e=6)
         def bar(*args, **kwargs):
             if args:
                 return args[0]
@@ -91,24 +91,39 @@ class TestObservable:
                     Observable.Call(
                         func=original_bar,
                         args=(),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
                     Observable.Call(
                         func=original_bar,
                         args=("a",),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
                     Observable.Call(
                         func=original_bar,
                         args=("b", "2"),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
@@ -116,8 +131,13 @@ class TestObservable:
                         func=original_bar,
                         args=("c",),
                         kwargs={"x": "y"},
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
             ]
@@ -183,7 +203,7 @@ class TestObservable:
         from griptape.common import observable
 
         class Foo:
-            @observable("one", 2, {"th": "ree"}, a="b", b=6)
+            @observable(a="one", b=2, c={"th": "ree"}, d="b", e=6)
             def bar(self, *args, **kwargs):
                 if args:
                     return args[0]
@@ -205,8 +225,13 @@ class TestObservable:
                         func=original_bar,
                         instance=foo,
                         args=(),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
@@ -214,8 +239,13 @@ class TestObservable:
                         func=original_bar,
                         instance=foo,
                         args=("a",),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
@@ -223,8 +253,13 @@ class TestObservable:
                         func=original_bar,
                         instance=foo,
                         args=("b", "2"),
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
                 call(
@@ -233,9 +268,23 @@ class TestObservable:
                         instance=foo,
                         args=("c",),
                         kwargs={"x": "y"},
-                        decorator_args=("one", 2, {"th": "ree"}),
-                        decorator_kwargs={"a": "b", "b": 6},
+                        decorator_kwargs={
+                            "a": "one",
+                            "b": 2,
+                            "c": {"th": "ree"},
+                            "d": "b",
+                            "e": 6,
+                        },
                     )
                 ),
             ]
         )
+
+    def test_positional_arg(self):
+        from griptape.common import observable
+
+        with pytest.raises(ValueError, match="Non-callable positional argument passed."):
+
+            @observable("foo")
+            def bar() -> None:
+                pass