Skip to content

Commit cabe0f7

Browse files
Merge pull request #630 from phenobarbital/orchestrator
Orchestrator
2 parents 3cf719d + a5fd5bf commit cabe0f7

Some content is hidden

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

51 files changed

+9662
-25
lines changed

parrot/bots/base.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
import asyncio
1111
from pydantic import BaseModel
1212
from ..memory import (
13-
ConversationTurn,
14-
ConversationHistory
13+
ConversationTurn
1514
)
1615
from ..models import AIMessage, StructuredOutputConfig
1716
from ..models.outputs import OutputMode
@@ -22,6 +21,7 @@
2221
)
2322
from .abstract import AbstractBot
2423
from ..models.status import AgentStatus
24+
from .middleware import PromptPipeline
2525

2626

2727
class BaseBot(AbstractBot):
@@ -364,6 +364,18 @@ async def invoke(
364364
content="Your request could not be processed due to security concerns.",
365365
metadata={'error': 'security_block'}
366366
)
367+
368+
# Apply prompt pipeline
369+
if self.prompt_pipeline and self._prompt_pipeline.has_middlewares:
370+
question = await self._prompt_pipeline.apply(
371+
question,
372+
context={
373+
'agent_name': self.name,
374+
'user_id': user_id,
375+
'session_id': session_id,
376+
'method': 'ask',
377+
}
378+
)
367379

368380
try:
369381
# Update status and trigger start event
@@ -563,6 +575,18 @@ async def ask(
563575
}
564576
)
565577

578+
# Apply prompt pipeline
579+
if self.prompt_pipeline and self._prompt_pipeline.has_middlewares:
580+
question = await self._prompt_pipeline.apply(
581+
question,
582+
context={
583+
'agent_name': self.name,
584+
'user_id': user_id,
585+
'session_id': session_id,
586+
'method': 'ask',
587+
}
588+
)
589+
566590
# Update status and trigger start event
567591
self.status = AgentStatus.WORKING
568592
self._trigger_event(
@@ -851,13 +875,25 @@ async def ask_stream(
851875
session_id=session_id,
852876
context={'method': 'ask_stream'}
853877
)
854-
except PromptInjectionException as e:
878+
except PromptInjectionException:
855879
yield (
856880
"Your request could not be processed due to security concerns. "
857881
"Please rephrase your question."
858882
)
859883
return
860884

885+
# Apply prompt pipeline
886+
if self.prompt_pipeline and self._prompt_pipeline.has_middlewares:
887+
question = await self._prompt_pipeline.apply(
888+
question,
889+
context={
890+
'agent_name': self.name,
891+
'user_id': user_id,
892+
'session_id': session_id,
893+
'method': 'ask',
894+
}
895+
)
896+
861897
default_max_tokens = self._llm_kwargs.get('max_tokens', None)
862898
max_tokens = kwargs.get('max_tokens', default_max_tokens)
863899
limit = kwargs.get('limit', self.context_search_limit)

parrot/bots/middleware.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PromptMiddleware:
1414
] = None
1515
enabled: bool = True
1616

17-
async def process(self, query: str, context: Dict[str, Any]) -> str:
17+
async def apply(self, query: str, context: Dict[str, Any]) -> str:
1818
if not self.enabled or not self.transform:
1919
return query
2020
return await self.transform(query, context)
@@ -34,11 +34,11 @@ def add(self, middleware: PromptMiddleware) -> None:
3434
def remove(self, name: str) -> None:
3535
self._middlewares = [m for m in self._middlewares if m.name != name]
3636

37-
async def process(self, query: str, context: Dict[str, Any] = None) -> str:
37+
async def apply(self, query: str, context: Dict[str, Any] = None) -> str:
3838
context = context or {}
3939
for mw in self._middlewares:
4040
try:
41-
query = await mw.process(query, context)
41+
query = await mw.apply(query, context)
4242
except Exception as e:
4343
self.logger.warning(
4444
f"Middleware '{mw.name}' failed: {e}, skipping"

parrot/bots/search.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
"""WebSearchAgent implementation for the ai-parrot framework."""
22
from string import Template
33
from typing import Optional, List, Any
4-
5-
from parrot.bots.agent import BasicAgent
6-
from parrot.models.responses import AIMessage
7-
4+
from ..models.responses import AIMessage
85
# Import default tools
9-
from parrot.tools.googlesearch import GoogleSearchTool
10-
from parrot.tools.googlesitesearch import GoogleSiteSearchTool
11-
from parrot.tools.ddgsearch import DdgSearchTool
12-
from parrot.tools.bingsearch import BingSearchTool
13-
from parrot.tools.serpapi import SerpApiSearchTool
6+
from ..tools.googlesearch import GoogleSearchTool
7+
from ..tools.googlesitesearch import GoogleSiteSearchTool
8+
from ..tools.ddgsearch import DdgSearchTool
9+
from ..tools.bingsearch import BingSearchTool
10+
from ..tools.serpapi import SerpApiSearchTool
11+
from .agent import BasicAgent
12+
from .middleware import PromptPipeline, PromptMiddleware
1413

1514
DEFAULT_CONTRASTIVE_PROMPT = """Based on following query: $query
1615
Below are search results about its COMPETITORS. Analyze ONLY the competitors:
@@ -40,6 +39,15 @@
4039
- **Sources Quality**: Assessment of information reliability"""
4140

4241

42+
DEFAULT_COMPETITOR_TRANSFORM_PROMPT = """You are a query transformation engine.
43+
Given a user query about a product or company, transform it into a competitor
44+
research query. NEVER return a query about the original product itself.
45+
Extract the product category, then generate a single search query focused
46+
on direct COMPETITORS and ALTERNATIVES.
47+
48+
Respond ONLY with the transformed search query string. Nothing else."""
49+
50+
4351
class WebSearchAgent(BasicAgent):
4452
"""An agent specialized in performing web searches.
4553
@@ -73,6 +81,8 @@ def __init__(
7381
contrastive_prompt: Optional[str] = None,
7482
synthesize: bool = False,
7583
synthesize_prompt: Optional[str] = None,
84+
competitor_search: bool = False,
85+
competitor_prompt: Optional[str] = None,
7686
**kwargs
7787
):
7888
"""Initialize the WebSearchAgent."""
@@ -81,7 +91,12 @@ def __init__(
8191
self.contrastive_prompt = contrastive_prompt or DEFAULT_CONTRASTIVE_PROMPT
8292
self.synthesize = synthesize
8393
self.synthesize_prompt = synthesize_prompt or DEFAULT_SYNTHESIZE_PROMPT
94+
self.competitor_search = competitor_search
95+
self._competitor_prompt = competitor_prompt or DEFAULT_COMPETITOR_TRANSFORM_PROMPT
8496

97+
# setup competitor search middleware if enabled:
98+
if self.competitor_search:
99+
self._setup_competitor_pipeline()
85100
# Provide a default list of web search tools if none is provided
86101
if tools is None:
87102
tools = [
@@ -101,6 +116,30 @@ def __init__(
101116
**kwargs
102117
)
103118

119+
def _setup_competitor_pipeline(self):
120+
if not self._prompt_pipeline:
121+
self._prompt_pipeline = PromptPipeline()
122+
self._prompt_pipeline.add(PromptMiddleware(
123+
name="competitor_transform",
124+
priority=10,
125+
transform=self._as_competitor_query
126+
))
127+
128+
async def _as_competitor_query(
129+
self, query: str, context: dict
130+
) -> str:
131+
"""Use LLM without tools to pivot query toward competitors."""
132+
self.logger.info(f"Transforming query to competitor search: {query}")
133+
async with self._llm as client:
134+
response = await client.ask(
135+
prompt=query,
136+
system_prompt=self._competitor_prompt,
137+
use_tools=False,
138+
)
139+
transformed = self._extract_text(response).strip()
140+
self.logger.info(f"Transformed query: {transformed}")
141+
return transformed
142+
104143
def _extract_text(self, response: AIMessage) -> str:
105144
"""Extract text content from an AIMessage response."""
106145
try:

parrot/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import click
33
from parrot.mcp.cli import mcp
44
from parrot.autonomous.cli import autonomous
5-
5+
from parrot.install.cli import install
66

77
@click.group()
88
def cli():
@@ -13,7 +13,7 @@ def cli():
1313
# Attach subcommands
1414
cli.add_command(mcp, name="mcp")
1515
cli.add_command(autonomous, name="autonomous")
16-
16+
cli.add_command(install, name="install")
1717

1818
if __name__ == "__main__":
1919
cli()

parrot/handlers/chat_interaction.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,61 @@ async def delete(self) -> web.Response:
277277
response={"message": f"Conversation {session_id} not found"},
278278
status=404,
279279
)
280+
281+
async def patch(self) -> web.Response:
282+
"""Delete a specific turn from a conversation."""
283+
storage = self._get_storage()
284+
if storage is None:
285+
self.error(
286+
response={"message": "Chat storage not available"},
287+
status=503,
288+
)
289+
290+
user_id = await self._get_user_id()
291+
if not user_id:
292+
self.error(
293+
response={"message": "User ID not found in session"},
294+
status=401,
295+
)
296+
297+
session_id = self.request.match_info.get("session_id")
298+
if not session_id:
299+
self.error(
300+
response={"message": "session_id is required in path"},
301+
status=400,
302+
)
303+
304+
try:
305+
body = await self.json_data()
306+
except Exception:
307+
body = {}
308+
309+
action = body.get("action")
310+
if action != "delete_turn":
311+
self.error(
312+
response={"message": f"Unknown action: {action}"},
313+
status=400,
314+
)
315+
316+
turn_id = body.get("turn_id")
317+
if not turn_id:
318+
self.error(
319+
response={"message": "turn_id is required"},
320+
status=400,
321+
)
322+
323+
deleted = await storage.delete_turn(
324+
session_id=session_id,
325+
turn_id=turn_id,
326+
)
327+
328+
if deleted:
329+
return self.json_response({
330+
"message": f"Turn {turn_id} deleted",
331+
"session_id": session_id,
332+
"turn_id": turn_id,
333+
})
334+
self.error(
335+
response={"message": f"Turn {turn_id} not found"},
336+
status=404,
337+
)

parrot/install/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Install command group for Parrot CLI."""

0 commit comments

Comments
 (0)