Skip to content

Commit 3cf719d

Browse files
Merge pull request #629 from phenobarbital/orchestrator
Orchestrator
2 parents 25cbc6d + 5987582 commit 3cf719d

File tree

6 files changed

+108
-15
lines changed

6 files changed

+108
-15
lines changed

parrot/bots/abstract.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
DEFAULT_ROLE,
2828
DEFAULT_CAPABILITIES,
2929
DEFAULT_BACKHISTORY,
30-
DEFAULT_RATIONALE,
31-
OUTPUT_SYSTEM_PROMPT
30+
DEFAULT_RATIONALE
3231
)
3332
from ..clients.base import (
3433
LLM_PRESETS,
@@ -61,7 +60,7 @@
6160
except ImportError:
6261
from ..security.prompt_injection import PromptInjectionDetector
6362
PYTECTOR_ENABLED = False
64-
from ..mcp import MCPEnabledMixin, MCPServerConfig
63+
from ..mcp import MCPEnabledMixin
6564
from ..security import (
6665
SecurityEventLogger,
6766
ThreatLevel,
@@ -70,11 +69,12 @@
7069
from .stores import LocalKBMixin
7170
from ..interfaces import ToolInterface, VectorInterface
7271
if TYPE_CHECKING:
73-
from ..stores import AbstractStore, supported_stores
72+
from ..stores import AbstractStore
7473
from ..stores.kb import AbstractKnowledgeBase
7574
from ..stores.models import StoreConfig
7675
from ..models.status import AgentStatus
7776
from .dynamic_values import dynamic_values
77+
from .middleware import PromptPipeline
7878

7979

8080
logging.getLogger(name='primp').setLevel(logging.INFO)
@@ -83,10 +83,19 @@
8383
logging.getLogger('markdown_it').setLevel(logging.CRITICAL)
8484

8585
# LLM parser regex:
86-
_LLM_PATTERN = re.compile(r'^([a-zA-Z0-9_-]+):(.+)$')
86+
_LLM_PATTERN = re.compile(
87+
r'^([a-zA-Z0-9_-]+):(.+)$'
88+
)
8789

8890

89-
class AbstractBot(MCPEnabledMixin, DBInterface, LocalKBMixin, ToolInterface, VectorInterface, ABC):
91+
class AbstractBot(
92+
MCPEnabledMixin,
93+
DBInterface,
94+
LocalKBMixin,
95+
ToolInterface,
96+
VectorInterface,
97+
ABC
98+
):
9099
"""AbstractBot.
91100
92101
This class is an abstract representation a base abstraction for all Chatbots.
@@ -97,6 +106,7 @@ class AbstractBot(MCPEnabledMixin, DBInterface, LocalKBMixin, ToolInterface, Vec
97106
'_llm',
98107
'_llm_config',
99108
'_llm_kwargs',
109+
'_prompt_pipeline'
100110
)
101111
# Define system prompt template
102112
system_prompt_template = BASIC_SYSTEM_PROMPT
@@ -175,6 +185,8 @@ def __init__(
175185
'description',
176186
self.description or f"{self.name} Chatbot"
177187
)
188+
# Prompt Pipeline:
189+
self._prompt_pipeline: PromptPipeline = None
178190

179191

180192
# Status and Events
@@ -332,6 +344,14 @@ def __init__(
332344
logger=self.logger
333345
)
334346

347+
@property
348+
def prompt_pipeline(self) -> Optional['PromptPipeline']:
349+
return self._prompt_pipeline
350+
351+
@prompt_pipeline.setter
352+
def prompt_pipeline(self, pipeline: 'PromptPipeline'):
353+
self._prompt_pipeline = pipeline
354+
335355
def _parse_llm_string(self, llm: str) -> Tuple[str, Optional[str]]:
336356
"""Parse 'provider:model' or plain provider string."""
337357
return match.groups() if (match := _LLM_PATTERN.match(llm)) else (llm, None)

parrot/bots/middleware.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Prompt middleware pipeline for query transformation."""
2+
from typing import Callable, Awaitable, Dict, Any, List
3+
from dataclasses import dataclass
4+
import logging
5+
6+
7+
@dataclass
8+
class PromptMiddleware:
9+
"""Single transformation step in the prompt pipeline."""
10+
name: str
11+
priority: int = 0 # Lower = runs first
12+
transform: Callable[
13+
[str, Dict[str, Any]], Awaitable[str]
14+
] = None
15+
enabled: bool = True
16+
17+
async def process(self, query: str, context: Dict[str, Any]) -> str:
18+
if not self.enabled or not self.transform:
19+
return query
20+
return await self.transform(query, context)
21+
22+
23+
class PromptPipeline:
24+
"""Ordered chain of prompt transformations applied before LLM call."""
25+
26+
def __init__(self):
27+
self._middlewares: List[PromptMiddleware] = []
28+
self.logger = logging.getLogger(__name__)
29+
30+
def add(self, middleware: PromptMiddleware) -> None:
31+
self._middlewares.append(middleware)
32+
self._middlewares.sort(key=lambda m: m.priority)
33+
34+
def remove(self, name: str) -> None:
35+
self._middlewares = [m for m in self._middlewares if m.name != name]
36+
37+
async def process(self, query: str, context: Dict[str, Any] = None) -> str:
38+
context = context or {}
39+
for mw in self._middlewares:
40+
try:
41+
query = await mw.process(query, context)
42+
except Exception as e:
43+
self.logger.warning(
44+
f"Middleware '{mw.name}' failed: {e}, skipping"
45+
)
46+
return query
47+
48+
@property
49+
def has_middlewares(self) -> bool:
50+
return bool(self._middlewares)

parrot/clients/google/generation.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,8 +1642,9 @@ async def generate_video_reel(
16421642
Generates a complete video reel from a high-level request.
16431643
Orchestrates:
16441644
1. Scene breakdown (if not provided)
1645-
2. Parallel generation of Music and Scenes (Image -> Video, Audio)
1646-
3. Assembly using MoviePy
1645+
2. Apply user-provided speech texts to scenes (if provided; otherwise no narration)
1646+
3. Parallel generation of Music and Scenes (Image -> Video, Audio)
1647+
4. Assembly using MoviePy
16471648
"""
16481649
self.logger.info(f"Starting Video Reel Generation: {request.prompt}")
16491650
start_time = time.time()
@@ -1659,7 +1660,21 @@ async def generate_video_reel(
16591660
self.logger.info("Breaking down prompt into scenes...")
16601661
request.scenes = await self._breakdown_prompt_to_scenes(request.prompt)
16611662

1662-
# 2. Parallel Generation
1663+
# 2. Apply user-provided speech texts to scenes (if provided)
1664+
# This overrides any narration_text that might exist in scenes
1665+
if request.speech:
1666+
for i, scene in enumerate(request.scenes):
1667+
if i < len(request.speech):
1668+
scene.narration_text = request.speech[i]
1669+
else:
1670+
# No speech provided for this scene
1671+
scene.narration_text = None
1672+
else:
1673+
# No speech provided at all - clear all narration
1674+
for scene in request.scenes:
1675+
scene.narration_text = None
1676+
1677+
# 3. Parallel Generation
16631678
# Task 1: Music
16641679
music_task = asyncio.create_task(
16651680
self._generate_reel_music(request, output_directory)
@@ -1685,7 +1700,7 @@ async def generate_video_reel(
16851700
if not valid_scene_outputs:
16861701
raise RuntimeError("All scene generations failed.")
16871702

1688-
# 3. Assembly
1703+
# 4. Assembly
16891704
final_video_path = await self._create_reel_assembly(
16901705
valid_scene_outputs,
16911706
music_path,
@@ -1718,9 +1733,10 @@ async def _breakdown_prompt_to_scenes(self, prompt: str) -> List[VideoReelScene]
17181733
- `background_prompt`: Detailed visual description for the background image.
17191734
- `foreground_prompt`: (Optional) Text describing a chart, KPI, or specific object to overlay. If not needed, omit.
17201735
- `video_prompt`: Instructions for animating the scene (e.g., "Slow pan up", "Cinematic zoom").
1721-
- `narration_text`: (Optional) A short sentence for the narrator to read.
17221736
- `duration`: Duration in seconds (usually 3-5s).
17231737
1738+
Note: Do NOT generate narration text. Narration/speech is provided separately by the user.
1739+
17241740
Return the result as a JSON array of objects matching this schema.
17251741
"""
17261742

@@ -1733,7 +1749,6 @@ async def _breakdown_prompt_to_scenes(self, prompt: str) -> List[VideoReelScene]
17331749
"background_prompt": {"type": "string"},
17341750
"foreground_prompt": {"type": "string"},
17351751
"video_prompt": {"type": "string"},
1736-
"narration_text": {"type": "string"},
17371752
"duration": {"type": "number"}
17381753
},
17391754
"required": ["background_prompt", "video_prompt", "duration"]

parrot/handlers/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,8 @@ async def post(self):
751751
with contextlib.suppress(AttributeError):
752752
request_session = self.request.session or await get_session(self.request)
753753

754-
# conversation (session_id)
755-
session_id = data.pop('session_id', None) or qs.get('session_id') or uuid.uuid4().hex
754+
# conversation (session_id) — already extracted by _get_user_session()
755+
session_id = user_session
756756
# Support method invocation via body or query parameter in addition to the
757757
# /{agent_id}/{method_name} route so clients don't need to construct a
758758
# different URL for maintenance operations like refresh_data.

parrot/models/google.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,14 @@ class VideoReelRequest(BaseModel):
461461
None,
462462
description="List of scenes. If not provided, they will be generated from the prompt."
463463
)
464+
speech: Optional[List[str]] = Field(
465+
None,
466+
description=(
467+
"List of speech/narration texts, one per scene. "
468+
"If provided, each text will be used as narration for the corresponding scene. "
469+
"If not provided, no narration will be added to the video reel."
470+
)
471+
)
464472
music_prompt: Optional[str] = Field(
465473
None,
466474
description="Description for the background music."

parrot/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"Complete Framework for AI Chatbots and Agents, "
66
"Supporting A2A, MCP, RAG and more."
77
)
8-
__version__ = "0.23.5"
8+
__version__ = "0.23.6"
99
__author__ = "Jesus Lara"
1010
__author_email__ = "jesuslarag@gmail.com"
1111
__license__ = "MIT"

0 commit comments

Comments
 (0)