Repository: MadcowD/ell

Issue:
bug: no results data in studio
when following the movie example from the docs, it seems like we can't see the results when we use structured outputs (note empty content block)

![CleanShot 2024-09-22 at 12 12 19@2x](https://github.com/user-attachments/assets/a92fd3d3-5233-43b7-bf3b-b358bf261878)

the code is: 

```
from pydantic import BaseModel, Field

from pw_ell.client import ell
from pw_ell.config import MODEL_DEFAULT


class MovieReview(BaseModel):
    title: str = Field(description="The title of the movie")
    rating: int = Field(description="The rating of the movie out of 10")
    summary: str = Field(description="A brief summary of the movie")


@ell.complex(model=MODEL_DEFAULT, response_format=MovieReview)
def generate_movie_review(movie: str):
    """You are a movie review generator. Given the name of a movie, you need to return a structured review."""
    return f"Generate a review for the movie {movie}"


review_message = generate_movie_review("The Godfather")
review = review_message.parsed
print(f"Movie: {review.title}, Rating: {review.rating}/10")
print(f"Summary: {review.summary}")

```

Discussion:

Patch:
diff --git a/docs/src/core_concepts/ell_studio.rst b/docs/src/core_concepts/ell_studio.rst
index 2435e77..62a1764 100644
--- a/docs/src/core_concepts/ell_studio.rst
+++ b/docs/src/core_concepts/ell_studio.rst
@@ -26,7 +26,7 @@ To start using Studio, run the following command in your terminal:
 
     ell-studio --storage ./logdir
 
-Then go to `http://localhost:5000 <http://localhost:5000>`_ to access the Studio interface.
+Then go to `http://localhost:5555 <http://localhost:5555>`_ to access the Studio interface.
 
 This command opens the Studio interface in your web browser, using the data stored in the specified directory (which should be the same directory you specified when initializing ell with `ell.init(store='./logdir')`). Since Studio runs locally, you can be assured that your sensitive prompt data never leaves your machine.
 
diff --git a/ell-studio/src/components/IORenderer.js b/ell-studio/src/components/IORenderer.js
index 3ef5c67..00f3e09 100644
--- a/ell-studio/src/components/IORenderer.js
+++ b/ell-studio/src/components/IORenderer.js
@@ -30,12 +30,9 @@ const preprocessData = (data, currentLevel = 0, typeMatchLevel = 0) => {
     
     if (typeMatchers.ContentBlock(data)) {
       const contentType = Object.keys(data).find(key => ['text', 'image', 'audio', 'tool_call', 'parsed', 'tool_result'].includes(key));
-      return { type: 'ContentBlock', content: preprocessData(data[contentType]) };
+      return { type: 'ContentBlock', [contentType]: preprocessData(data[contentType]) };
     }
-    
-    // ... other type matchers ...
   }
-  
   if (Array.isArray(data)) {
     return data.map(item => preprocessData(item, currentLevel + 1, typeMatchLevel));
   }
@@ -150,8 +147,6 @@ const renderNdarray = (data) => {
     // Add more types as needed
     arrayData = Array.from(decodedData);
   }
-  console.log(arrayData)
-
   let displayData;
   if (numElements > 3) {
     displayData = arrayData.slice(0, 3);
@@ -159,7 +154,6 @@ const renderNdarray = (data) => {
   } else {
     displayData = arrayData;
   }
-  console.log(displayData[0])
   return (
     <span className="text-indigo-400">
       np.array(
diff --git a/ell-studio/src/components/invocations/details/InvocationDataPane.js b/ell-studio/src/components/invocations/details/InvocationDataPane.js
index 57fe530..c0c2106 100644
--- a/ell-studio/src/components/invocations/details/InvocationDataPane.js
+++ b/ell-studio/src/components/invocations/details/InvocationDataPane.js
@@ -30,7 +30,7 @@ const InvocationDataPane = ({ invocation }) => {
            invocation.contents?.results !== undefined;
   }, [invocation.contents?.results]);
 
-  const renderCodeSection = (title, content, expanded, setExpanded) => (
+  const renderCodeSection = (title, content, expanded, setExpanded, typeMatchLevel) => (
     <CodeSection
       title={title}
       raw={isExternalLoading ? "Loading..." : JSON.stringify(content, null, 2)}
@@ -47,7 +47,7 @@ const InvocationDataPane = ({ invocation }) => {
         {isExternalLoading ? (
           <SkeletonLoader />
         ) : (
-          <IORenderer content={content} typeMatchLevel={1} inline={false} />
+          <IORenderer content={content} typeMatchLevel={typeMatchLevel} inline={false} />
         )}
       </div>
     </CodeSection>
@@ -59,14 +59,16 @@ const InvocationDataPane = ({ invocation }) => {
         "Input",
         invocation.contents?.params,
         inputExpanded,
-        setInputExpanded
+        setInputExpanded,
+        1
       )}
 
       {(hasResults || isExternalLoading) && renderCodeSection(
         "Results",
         invocation.contents?.results,
         outputExpanded,
-        setOutputExpanded
+        setOutputExpanded,
+        0
       )}
     </div>
   );
diff --git a/ell-studio/src/hooks/useBackend.js b/ell-studio/src/hooks/useBackend.js
index 6d10c54..7b42134 100644
--- a/ell-studio/src/hooks/useBackend.js
+++ b/ell-studio/src/hooks/useBackend.js
@@ -4,13 +4,13 @@ import { useEffect, useState } from "react";
 
 
 const API_BASE_URL = process.env.NODE_ENV === 'development' 
-  ? "http://localhost:5000" 
+  ? "http://localhost:5555" 
   : window.location.origin;
 
 // Function to get WebSocket URL
 const getWebSocketURL = () => {
   const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-  const host = process.env.NODE_ENV === 'development' ? 'localhost:5000' : window.location.host;
+  const host = process.env.NODE_ENV === 'development' ? 'localhost:5555' : window.location.host;
   console.log(host)
   return `${protocol}//${host}/ws`;
   
diff --git a/src/ell/providers/openai.py b/src/ell/providers/openai.py
index e8b17be..57c6ee1 100644
--- a/src/ell/providers/openai.py
+++ b/src/ell/providers/openai.py
@@ -132,11 +132,13 @@ try:
                     if hasattr(message, "parsed"):
                         if (parsed := message.parsed): 
                             content_blocks.append(ContentBlock(parsed=parsed)) #XXX: Origin tracing
+                            if logger: logger(parsed.model_dump_json())
                     else:
                         if (content := message.content):
                             content_blocks.append(
                                 ContentBlock(
                                     text=_lstr(content=content,origin_trace=origin_id)))
+                            if logger: logger(content)
                         if (tool_calls := message.tool_calls):
                             for tool_call in tool_calls:
                                 matching_tool = ell_call.get_tool_by_name(tool_call.function.name)
@@ -151,6 +153,7 @@ try:
                                         )
                                     )
                                 )
+                                if logger: logger(tool_call)
                     messages.append(Message(role=role, content=content_blocks))
             return messages, metadata
 
diff --git a/src/ell/studio/__main__.py b/src/ell/studio/__main__.py
index 06a0354..4e29908 100644
--- a/src/ell/studio/__main__.py
+++ b/src/ell/studio/__main__.py
@@ -17,12 +17,12 @@ def main():
     parser.add_argument("--pg-connection-string", default=None,
                         help="PostgreSQL connection string (default: None)")
     parser.add_argument("--host", default="127.0.0.1", help="Host to run the server on")
-    parser.add_argument("--port", type=int, default=5000, help="Port to run the server on")
+    parser.add_argument("--port", type=int, default=5555, help="Port to run the server on")
     parser.add_argument("--dev", action="store_true", help="Run in development mode")
     args = parser.parse_args()
 
     if args.dev:
-        assert args.port == 5000, "Port must be 5000 in development mode"
+        assert args.port == 5555, "Port must be 5000 in development mode"
 
     config = Config.create(storage_dir=args.storage_dir,
                     pg_connection_string=args.pg_connection_string)
diff --git a/src/ell/types/message.py b/src/ell/types/message.py
index 68e0a90..32eca35 100644
--- a/src/ell/types/message.py
+++ b/src/ell/types/message.py
@@ -152,6 +152,13 @@ class ContentBlock(BaseModel):
 
             return cls(image=content)
         raise ValueError(f"Invalid content type: {type(content)}")
+    
+    @field_serializer('parsed')
+    def serialize_parsed(self, value: Optional[BaseModel], _info):
+        if value is None:
+            return None
+        return value.model_dump(exclude_none=True, exclude_unset=True)
+    
 
     @field_validator('image')
     @classmethod
diff --git a/src/ell/util/serialization.py b/src/ell/util/serialization.py
index dd5dfce..c91628f 100644
--- a/src/ell/util/serialization.py
+++ b/src/ell/util/serialization.py
@@ -64,7 +64,8 @@ pydantic_ltype_aware_cattr.register_unstructure_hook(
     BaseModel,
     lambda obj: obj.model_dump(exclude_none=True, exclude_unset=True)
 )
-# Register hooks for complex types (deserialization)
+
+
 
 
 def get_immutable_vars(vars_dict):
@@ -103,6 +104,8 @@ def prepare_invocation_params(params):
     
     # Thisis because we wneed the caching to work on the hash of a cleaned and serialized object.
     jstr = json.dumps(cleaned_invocation_params, sort_keys=True, default=repr)
+    print(cleaned_invocation_params, invocation_params)
+    print(jstr)
 
     consumes = set()
     import re


Test patch:
diff --git a/tests/test_message_type.py b/tests/test_message_type.py
index 1119115..6b784b6 100644
--- a/tests/test_message_type.py
+++ b/tests/test_message_type.py
@@ -45,6 +45,16 @@ def test_content_block_coerce_base_model():
     assert result.parsed == formatted_response
     assert result.type == "parsed"
 
+
+def test_serialization_of_content_block_with_parsed():
+    class DummyFormattedResponse(BaseModel):
+        field1: str
+        field2: int
+
+    msg = Message(role="user", content=[ContentBlock(parsed=DummyFormattedResponse(field1="test", field2=42))])
+    assert msg.model_dump(exclude_none=True, exclude_unset=True) == {'role': 'user', 'content': [{'parsed': {'field1': 'test', 'field2': 42}}]}
+
+
 def test_content_block_coerce_content_block():
     original_block = ContentBlock(text="Original content")
     result = ContentBlock.coerce(original_block)