Skip to content

Commit df05ace

Browse files
Merge pull request Pipelex#122 from Pipelex/release/v0.4.7
Release/v0.4.7
2 parents 45e4e8e + c312e9f commit df05ace

56 files changed

Lines changed: 1568 additions & 216 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/prompt-templates.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description:
3-
globs: **/pipelex_libraries/pipelines/toml/**/*.toml
3+
globs: pipelex/libraries/pipelines/**/*.toml
44
alwaysApply: false
55
---
66
This rule explains how to write prompt templates in PipeLLM definitions. These prompts will be rendered and become the user_text or the system_prompt part of LLM chat completion queries.

.cursor/rules/pytest.mdc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ pipe_output: PipeOutput = await pipe_router.run_pipe(
104104
pipe_code="pipe_name",
105105
pipe_run_params=PipeRunParamsFactory.make_run_params(),
106106
working_memory=working_memory,
107-
job_metadata=JobMetadata(
108-
109-
top_job_id=cast(str, request.node.originalname), # type: ignore
110-
),
107+
job_metadata=JobMetadata(),
111108
)
112109
```
113110

.cursor/rules/standards.mdc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ alwaysApply: true
77

88
This document outlines the coding standards and quality control procedures that must be followed when contributing to this project.
99

10+
## Style
11+
12+
Always use type hints. Use the types with Uppercase first letter for types like Dict[], List[] etc.
13+
1014
## Code Quality Checks
1115

1216
### Linting and Type Checking

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [v0.4.7] - 2025-06-26
4+
5+
- Added an API serializer: introducing the `compact_memory`, a new way to encode/decode the working memory as json, for the API.
6+
- Added `StorageProviderAbstract`
7+
- When creating a Concept with no structure specified and no explicit `refines`, set it to refine `native.Text`
8+
- `JobMetadata`: added `job_name`. Removed `top_job_id` and `wfid`
9+
- `PipeOutput`: added `pipeline_run_id`
10+
311
## [v0.4.6] - 2025-06-24
412

513
- Changed the link to the doc in the `README.md`: https://docs.pipelex.com

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,11 +406,11 @@ li: lock install
406406

407407
check-TODOs: env
408408
$(call PRINT_TITLE,"Checking for TODOs")
409-
$(VENV_RUFF) check --select=TD -v .
409+
$(VENV_RUFF) check --select=TD .
410410

411411
fix-unused-imports: env
412412
$(call PRINT_TITLE,"Fixing unused imports")
413-
$(VENV_RUFF) check --select=F401 --fix -v .
413+
$(VENV_RUFF) check --select=F401 --fix .
414414

415415
doc: env
416416
$(call PRINT_TITLE,"Serving documentation with mkdocs")

mkdocs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ extra:
3939
link: https://github.com/Pipelex/pipelex
4040
name: Pipelex on GitHub
4141
- icon: fontawesome/brands/python
42-
link: https://pypi.org/project/kajson/
43-
name: kajson on PyPI
42+
link: https://pypi.org/project/pipelex/
43+
name: pipelex on PyPI
4444
- icon: fontawesome/brands/twitter
4545
link: https://x.com/PipelexAI
4646
name: Pipelex on X

pipelex/client/api_serializer.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from datetime import datetime
2+
from decimal import Decimal
3+
from enum import Enum
4+
from pathlib import Path
5+
from typing import Any, Dict, List, cast
6+
7+
from pipelex.client.protocol import CompactMemory
8+
from pipelex.core.concept_native import NativeConcept
9+
from pipelex.core.pipe_output import PipeOutput
10+
from pipelex.core.stuff_content import StuffContent, TextContent
11+
from pipelex.core.stuff_factory import StuffContentFactory
12+
from pipelex.core.working_memory import WorkingMemory
13+
from pipelex.exceptions import ApiSerializationError
14+
15+
16+
class ApiSerializer:
17+
"""Handles API-specific serialization with kajson, datetime formatting, and cleanup."""
18+
19+
# Fixed datetime format for API consistency
20+
API_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
21+
FIELDS_TO_SKIP = ("__class__", "__module__")
22+
23+
@classmethod
24+
def serialize_working_memory_for_api(cls, working_memory: WorkingMemory) -> CompactMemory:
25+
"""
26+
Convert WorkingMemory to API-ready format using kajson with proper datetime handling.
27+
28+
Args:
29+
working_memory: The WorkingMemory to serialize
30+
31+
Returns:
32+
Dict ready for API transmission with datetime strings and no __class__/__module__
33+
"""
34+
compact_memory: CompactMemory = {}
35+
36+
for stuff_name, stuff in working_memory.root.items():
37+
if stuff.concept_code == NativeConcept.TEXT.code:
38+
stuff_content = cast(TextContent, stuff.content)
39+
item_dict: Dict[str, Any] = {
40+
"concept_code": stuff.concept_code,
41+
"content": stuff_content.text,
42+
}
43+
else:
44+
content_dict = stuff.content.model_dump(serialize_as_any=True)
45+
clean_content = cls._clean_and_format_content(content_dict)
46+
47+
item_dict = {
48+
"concept_code": stuff.concept_code,
49+
"content": clean_content,
50+
}
51+
52+
compact_memory[stuff_name] = item_dict
53+
54+
return compact_memory
55+
56+
@classmethod
57+
def serialize_pipe_output_for_api(cls, pipe_output: PipeOutput) -> CompactMemory:
58+
"""
59+
Convert PipeOutput to API-ready format.
60+
61+
Args:
62+
pipe_output: The PipeOutput to serialize
63+
64+
Returns:
65+
Dict ready for API transmission
66+
"""
67+
return {"compact_memory": cls.serialize_working_memory_for_api(pipe_output.working_memory)}
68+
69+
@classmethod
70+
def _clean_and_format_content(cls, content: Any) -> Any:
71+
"""
72+
Recursively clean content by removing the fields in FIELDS_TO_SKIP and formatting datetimes.
73+
74+
Args:
75+
content: Content to clean
76+
77+
Returns:
78+
Cleaned content with formatted datetimes
79+
"""
80+
if isinstance(content, dict):
81+
cleaned: Dict[str, Any] = {}
82+
content_dict = cast(Dict[str, Any], content)
83+
for key in content_dict:
84+
if key in cls.FIELDS_TO_SKIP:
85+
continue
86+
cleaned[key] = cls._clean_and_format_content(content_dict[key])
87+
return cleaned
88+
elif isinstance(content, list):
89+
cleaned_list: List[Any] = []
90+
content_list = cast(List[Any], content)
91+
for idx in range(len(content_list)):
92+
cleaned_list.append(cls._clean_and_format_content(content_list[idx]))
93+
return cleaned_list
94+
elif isinstance(content, datetime):
95+
return content.strftime(cls.API_DATETIME_FORMAT)
96+
elif isinstance(content, Enum):
97+
return content.value # Convert enum to its value
98+
elif isinstance(content, Decimal):
99+
return float(content) # Convert Decimal to float for JSON compatibility
100+
elif isinstance(content, Path):
101+
return str(content) # Convert Path to string representation
102+
else:
103+
return content
104+
105+
@classmethod
106+
def make_stuff_content_from_api_data(cls, concept_code: str, value: Dict[str, Any] | str) -> StuffContent:
107+
"""
108+
Create StuffContent from API data using concept code.
109+
110+
Args:
111+
concept_code: The concept code to determine the content type
112+
value: The content value from API
113+
114+
Returns:
115+
StuffContent instance
116+
117+
Raises:
118+
ApiSerializationError: If concept cannot be resolved or content creation fails
119+
"""
120+
try:
121+
return StuffContentFactory.make_stuffcontent_from_concept_code_with_fallback(concept_code=concept_code, value=value)
122+
123+
except Exception as exc:
124+
raise ApiSerializationError(f"Failed to create StuffContent for concept '{concept_code}': {exc}") from exc

pipelex/client/client.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from typing import Any, Optional, cast
1+
from typing import Any, Optional
22

33
import httpx
4-
from kajson import kajson
54
from typing_extensions import override
65

7-
from pipelex.client.protocol import PipelexProtocol, PipelineRequest, PipelineResponse
6+
from pipelex.client.pipeline_request_factory import PipelineRequestFactory
7+
from pipelex.client.pipeline_response_factory import PipelineResponseFactory
8+
from pipelex.client.protocol import PipelexProtocol, PipelineResponse
89
from pipelex.core.pipe_run_params import PipeOutputMultiplicity
910
from pipelex.core.working_memory import WorkingMemory
1011
from pipelex.exceptions import ClientAuthenticationError
@@ -78,14 +79,14 @@ async def execute_pipeline(
7879
output_multiplicity: Optional[PipeOutputMultiplicity] = None,
7980
dynamic_output_concept_code: Optional[str] = None,
8081
) -> PipelineResponse:
81-
pipeline_request = PipelineRequest(
82+
pipeline_request = PipelineRequestFactory.make_from_working_memory(
8283
working_memory=working_memory,
8384
output_name=output_name,
8485
output_multiplicity=output_multiplicity,
8586
dynamic_output_concept_code=dynamic_output_concept_code,
8687
)
87-
response = await self._make_api_call(f"pipelex/v1/pipeline/{pipe_code}/execute", request=kajson.dumps(pipeline_request))
88-
return cast(PipelineResponse, kajson.loads(response))
88+
response = await self._make_api_call(f"v1/pipeline/{pipe_code}/execute", request=pipeline_request.model_dump_json())
89+
return PipelineResponseFactory.make_from_api_response(response)
8990

9091
@override
9192
async def start_pipeline(
@@ -96,11 +97,11 @@ async def start_pipeline(
9697
output_multiplicity: Optional[PipeOutputMultiplicity] = None,
9798
dynamic_output_concept_code: Optional[str] = None,
9899
) -> PipelineResponse:
99-
pipeline_request = PipelineRequest(
100+
pipeline_request = PipelineRequestFactory.make_from_working_memory(
100101
working_memory=working_memory,
101102
output_name=output_name,
102103
output_multiplicity=output_multiplicity,
103104
dynamic_output_concept_code=dynamic_output_concept_code,
104105
)
105-
response = await self._make_api_call(f"pipelex/v1/pipeline/{pipe_code}/start", request=kajson.dumps(pipeline_request))
106-
return cast(PipelineResponse, kajson.loads(response))
106+
response = await self._make_api_call(f"v1/pipeline/{pipe_code}/start", request=pipeline_request.model_dump_json())
107+
return PipelineResponseFactory.make_from_api_response(response)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Any, Dict, Optional
2+
3+
from pipelex.client.api_serializer import ApiSerializer
4+
from pipelex.client.protocol import COMPACT_MEMORY_KEY, CompactMemory, PipelineRequest
5+
from pipelex.core.pipe_run_params import PipeOutputMultiplicity
6+
from pipelex.core.stuff_factory import StuffFactory
7+
from pipelex.core.working_memory import WorkingMemory
8+
from pipelex.core.working_memory_factory import WorkingMemoryFactory
9+
10+
11+
class PipelineRequestFactory:
12+
"""Factory class for creating PipelineRequest objects from WorkingMemory."""
13+
14+
@staticmethod
15+
def make_from_working_memory(
16+
working_memory: Optional[WorkingMemory] = None,
17+
output_name: Optional[str] = None,
18+
output_multiplicity: Optional[PipeOutputMultiplicity] = None,
19+
dynamic_output_concept_code: Optional[str] = None,
20+
) -> PipelineRequest:
21+
"""
22+
Create a PipelineRequest from a WorkingMemory object.
23+
24+
Args:
25+
working_memory: The WorkingMemory to convert
26+
output_name: Name of the output slot to write to
27+
output_multiplicity: Output multiplicity setting
28+
dynamic_output_concept_code: Override for the dynamic output concept code
29+
30+
Returns:
31+
PipelineRequest with the working memory serialized to reduced format
32+
"""
33+
compact_memory = None
34+
if working_memory is not None:
35+
compact_memory = ApiSerializer.serialize_working_memory_for_api(working_memory)
36+
37+
return PipelineRequest(
38+
compact_memory=compact_memory,
39+
output_name=output_name,
40+
output_multiplicity=output_multiplicity,
41+
dynamic_output_concept_code=dynamic_output_concept_code,
42+
)
43+
44+
@staticmethod
45+
def make_working_memory_from_reduced(compact_memory: Optional[CompactMemory]) -> WorkingMemory:
46+
"""
47+
Create a WorkingMemory from a reduced memory dictionary.
48+
49+
Args:
50+
compact_memory: Dictionary in the format from API
51+
52+
Returns:
53+
WorkingMemory object reconstructed from the reduced format
54+
"""
55+
working_memory = WorkingMemoryFactory.make_empty()
56+
if compact_memory is None:
57+
return working_memory
58+
59+
for stuff_key, stuff_data in compact_memory.items():
60+
concept_code = stuff_data.get("concept_code", "")
61+
content_value = stuff_data.get("content", {})
62+
63+
# Use API serializer to create content
64+
content = ApiSerializer.make_stuff_content_from_api_data(concept_code=concept_code, value=content_value)
65+
66+
# Create stuff directly
67+
stuff = StuffFactory.make_stuff(concept_str=concept_code, name=stuff_key, content=content)
68+
69+
working_memory.add_new_stuff(name=stuff_key, stuff=stuff)
70+
71+
return working_memory
72+
73+
@staticmethod
74+
def make_request_from_body(request_body: Dict[str, Any]) -> PipelineRequest:
75+
"""
76+
Create a PipelineRequest from raw request body dictionary.
77+
78+
Args:
79+
request_body: Raw dictionary from API request body
80+
81+
Returns:
82+
PipelineRequest object with dictionary working_memory
83+
"""
84+
return PipelineRequest(
85+
compact_memory=request_body.get(COMPACT_MEMORY_KEY),
86+
output_name=request_body.get("output_name"),
87+
output_multiplicity=request_body.get("output_multiplicity"),
88+
dynamic_output_concept_code=request_body.get("dynamic_output_concept_code"),
89+
)

0 commit comments

Comments
 (0)