Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export default defineConfig({
},
{
text: 'Agent',
items: [{ text: 'Friday', link: '/agent/friday' }],
items: [
{ text: 'Friday', link: '/agent/friday' },
{ text: 'Planning', link: '/agent/planning' },
],
},
],
},
Expand Down Expand Up @@ -117,6 +120,7 @@ export default defineConfig({
text: '智能体',
items: [
{ text: 'Friday', link: '/zh_CN/agent/friday' },
{ text: '计划管理', link: '/zh_CN/agent/planning' },
],
},
],
Expand Down
19 changes: 19 additions & 0 deletions docs/tutorial/en/agent/Planning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Planning

When facing complex problems, Friday automatically creates a task plan and executes it step by step.

- Supports inserting, deleting, and editing subtasks.
- Supports modifying the current status of subtasks.
- Supports reordering unexecuted subtasks via `drag and drop`.

> **⚠️ Note: Completed subtasks do not support any modifications except deletion.**

## Edit Subtasks

For existing subtasks, use the insert, edit, and delete buttons shown below.
![plan edit buttons](assets/plan_edit.png)

## Modify Subtask Status

Click the subtask status icon to change its current status.
![plan status edit](assets/plan_state.png)
Binary file added docs/tutorial/en/agent/assets/plan_edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/tutorial/en/agent/assets/plan_state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docs/tutorial/zh_CN/agent/Planning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 计划管理

Friday在面对复杂问题时会自动创建任务计划,并按计划分步执行

- 支持用户对子任务的插入,删除,修改。
- 支持用户修改子任务当前状态。
- 支持用户通过`拖拽`修改未执行子任务的执行顺序。

> **⚠️ 注意:状态为已完成的子任务除了删除之外不支持任何修改操作**

## 编辑子任务

对于已经存在的子任务,可以使用以下插入,修改,删除按钮进行编辑。
![plan 编辑按钮](assets/plan_edit.png)

## 修改子任务当前状态

点击子任务状态图标,可以修改当前状态
![plan 状态编辑](assets/plan_state.png)
Binary file added docs/tutorial/zh_CN/agent/assets/plan_edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/tutorial/zh_CN/agent/assets/plan_state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/app/friday/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ def get_args() -> Namespace:
default={},
help="A JSON string representing a dictionary of keyword arguments to pass to the LLM generate method.",
)
args = parser.parse_args()
args, _ = parser.parse_known_args()
return args
62 changes: 62 additions & 0 deletions packages/app/friday/clean_invalid_plans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Clean invalid plans from storage.

This script removes plans with non-completed states (for example, 'in_progress') from
storage, as these should only exist in session, not in storage.
Storage should only contain completed ('done' or 'abandoned') plans.
"""
import asyncio
import sys
import os
import logging

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger(__name__)

# Add parent directory to path to enable direct execution
# Recommended: Run as module from project root: python -m packages.app.friday.clean_invalid_plans
sys.path.insert(0, os.path.dirname(__file__))

from plan_manager import JSONPlanStorage


async def clean_invalid_plans():
"""Remove invalid plans from storage."""
plan_storage = JSONPlanStorage()

# Get all plans
plans = await plan_storage.get_plans()

logger.info(f"Found {len(plans)} plans in storage")

# Find invalid plans (not done or abandoned)
invalid_plans = [p for p in plans if p.state not in ['done', 'abandoned']]

if not invalid_plans:
logger.info("No invalid plans found. Storage is clean!")
return

logger.info(f"\nFound {len(invalid_plans)} invalid plans:")
for plan in invalid_plans:
logger.info(f" - {plan.name} (state: {plan.state}, id: {plan.id})")

# Remove invalid plans
for plan in invalid_plans:
try:
await plan_storage.delete_plan(plan.id)
logger.info(f"✓ Deleted: {plan.name}")
except Exception as e:
logger.error(f"✗ Failed to delete {plan.name}: {e}")

logger.info(f"\nCleaning complete. Removed {len(invalid_plans)} invalid plans.")


if __name__ == "__main__":
asyncio.run(clean_invalid_plans())

51 changes: 51 additions & 0 deletions packages/app/friday/hook.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
"""The hooks for the agent"""
import asyncio
from typing import Any

import requests
from agentscope.agent import AgentBase
from agentscope.plan import Plan, PlanNotebook


def studio_pre_print_hook(self: AgentBase, kwargs: dict[str, Any]) -> None:
Expand Down Expand Up @@ -50,3 +52,52 @@ def studio_post_reply_hook(self: AgentBase, *args, **kwargs) -> None:
continue

raise e from None


async def push_plan_hook(
notebook: PlanNotebook,
plan: Plan | None,
) -> None:
"""Push plan updates to the studio frontend via HTTP request.

This hook is triggered whenever the plan changes (create, update, delete).
It sends the current plan, and if plan is None (finished), also sends historical plans.

Args:
notebook: The PlanNotebook instance
plan: The current plan (None if plan is finished/cleared)
"""
if not hasattr(push_plan_hook, 'url'):
return

# Get current plan data
current_plan_data = plan.model_dump() if plan else None

# If plan is None (finished), also load historical plans in the same request
historical_plans_data = []
if plan is None:
try:
historical_plans = await notebook.storage.get_plans()
historical_plans_data = [p.model_dump() for p in historical_plans]
except Exception as e:
print(f"Error fetching historical plans: {e}")

n_retry = 0
while True:
try:
res = await asyncio.to_thread(
requests.post,
f"{push_plan_hook.url}/trpc/pushCurrentPlanToFridayApp",
json={
"currentPlan": current_plan_data,
"historicalPlans": historical_plans_data,
},
)
res.raise_for_status()
break
except Exception as e:
if n_retry < 3:
n_retry += 1
continue
print(f"Failed to push plan after {n_retry} retries: {e}")
break
29 changes: 25 additions & 4 deletions packages/app/friday/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
insert_text_file,
view_text_file,
)
from agentscope.plan import PlanNotebook, Plan, SubTask

from hook import (
studio_pre_print_hook,
studio_post_reply_hook,
push_plan_hook,
)
from args import get_args
from model import get_model, get_formatter
Expand All @@ -31,14 +33,18 @@
view_agentscope_readme,
view_agentscope_faq,
)
from utils.common import get_local_file_path
from utils.common import get_local_file_path, save_studio_url
from utils.connect import StudioConnect
from utils.constants import FRIDAY_SESSION_ID
from plan_manager import JSONPlanStorage


async def main():
args = get_args()

# Save studio URL to config file for API access
save_studio_url(args.studio_url)

studio_pre_print_hook.url = args.studio_url

# Forward message to the studio
Expand Down Expand Up @@ -92,6 +98,18 @@ async def main():
model = get_model(args.llmProvider, args.modelName, args.apiKey, args.clientKwargs, args.generateKwargs)
formatter = get_formatter(args.llmProvider)

# Create PlanNotebook with JSON storage
plan_storage = JSONPlanStorage()
plan_notebook = PlanNotebook(storage=plan_storage)

# Register plan change hook to push plan updates to frontend
# When plan is finished (plan=None), it also pushes historical plans in the same request
push_plan_hook.url = args.studio_url
plan_notebook.register_plan_change_hook(
"push_plan",
push_plan_hook,
)

# Create the ReAct agent
agent = ReActAgent(
name="Friday",
Expand Down Expand Up @@ -135,6 +153,7 @@ async def main():
memory=InMemoryMemory(),
max_iters=50,
enable_meta_tool=True,
plan_notebook=plan_notebook,
)

path_dialog_history = get_local_file_path("")
Expand All @@ -150,7 +169,8 @@ async def main():

await session.load_session_state(
session_id=FRIDAY_SESSION_ID,
friday=agent
friday=agent,
plan_notebook=plan_notebook
)

# The socket is used for realtime steering
Expand All @@ -159,10 +179,11 @@ async def main():
await agent(Msg("user", json5.loads(args.query), "user"))
await socket.disconnect()

# Save dialog history
# Save dialog history and plan notebook state
await session.save_session_state(
session_id=FRIDAY_SESSION_ID,
friday=agent
friday=agent,
plan_notebook=plan_notebook
)

if __name__ == '__main__':
Expand Down
6 changes: 6 additions & 0 deletions packages/app/friday/plan_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""Plan manager module for Friday agent."""
from .storage import JSONPlanStorage
from .api import get_plans_from_storage

__all__ = ['JSONPlanStorage', 'get_plans_from_storage']
Loading
Loading