Refactor Workflow and Chat Architecture (#68)

* refactor: overhaul workflow and chat architecture

- Separate Chat and Workflow API endpoints and database models
- Use JSONB to store workflow execution context in Postgres
- Convert workflow engine to use pydantic-ai execution graphs inside a Ray task
- Update frontend React components to support standalone workflow creation
- Remove obsolete and broken workflow runner tests

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* refactor: overhaul workflow and chat architecture

- Separate Chat and Workflow API endpoints and database models
- Use JSONB to store workflow execution context in Postgres
- Convert workflow engine to use pydantic-ai execution graphs inside a Ray task
- Update frontend React components to support standalone workflow creation
- Remove obsolete and broken workflow runner tests

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* refactor: overhaul workflow and chat architecture

- Separate Chat and Workflow API endpoints and database models
- Use JSONB to store workflow execution context in Postgres
- Convert workflow engine to use pydantic-ai execution graphs inside a Ray task
- Update frontend React components to support standalone workflow creation
- Move workflow_engine inside workflow package to keep core root clean
- Remove obsolete and broken workflow runner tests

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
This commit is contained in:
2026-05-12 15:47:17 +08:00
committed by GitHub
parent ee9bbbf676
commit ff1ede47a0
33 changed files with 995 additions and 412 deletions
@@ -0,0 +1,82 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from sqlalchemy import select, func
from typing import List
from kilostar.core.postgres_database.model.chat_history import (
ChatHistoryRegister,
ChatHistoryMessage,
)
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
from ulid import ULID
class ChatHistoryDatabase:
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
self.async_session_maker = async_session_maker
async def create_chat_session(
self, user_id: str, title: str = "新对话"
) -> ChatHistoryRegister:
async with self.async_session_maker() as session:
chat_id = str(ULID())
chat = ChatHistoryRegister(chat_id=chat_id, user_id=user_id, title=title)
session.add(chat)
await session.commit()
await session.refresh(chat)
return chat
async def list_chat_sessions(self, user_id: str) -> List[ChatHistoryRegister]:
async with self.async_session_maker() as session:
statement = (
select(ChatHistoryRegister)
.where(ChatHistoryRegister.user_id == user_id)
.order_by(ChatHistoryRegister.updated_at.desc())
)
results = await session.execute(statement)
return results.scalars().all()
async def add_chat_message(
self, chat_id: str, message: str, message_owner: str
) -> ChatHistoryMessage:
async with self.async_session_maker() as session:
msg_id = str(ULID())
msg = ChatHistoryMessage(
message_id=msg_id,
chat_id=chat_id,
message=message,
message_owner=message_owner,
)
session.add(msg)
# Update the chat session's updated_at
statement = select(ChatHistoryRegister).where(
ChatHistoryRegister.chat_id == chat_id
)
results = await session.execute(statement)
chat = results.scalar_one_or_none()
if chat:
chat.updated_at = func.now()
await session.commit()
await session.refresh(msg)
return msg
async def list_chat_messages(self, chat_id: str) -> List[ChatHistoryMessage]:
async with self.async_session_maker() as session:
statement = (
select(ChatHistoryMessage)
.where(ChatHistoryMessage.chat_id == chat_id)
.order_by(ChatHistoryMessage.created_at.asc())
)
results = await session.execute(statement)
return results.scalars().all()
@@ -0,0 +1,96 @@
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from sqlalchemy import select
from typing import List, Optional
from kilostar.core.postgres_database.model.workflow import (
Workflow,
WorkflowContextModel,
)
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
class WorkflowDatabase:
def __init__(self, async_session_maker: async_sessionmaker[AsyncSession]):
self.async_session_maker = async_session_maker
async def create_workflow(
self, trace_id: str, user_id: str, title: str, command: str
) -> Workflow:
async with self.async_session_maker() as session:
wf = Workflow(
trace_id=trace_id,
user_id=user_id,
title=title,
command=command,
status="creating",
)
session.add(wf)
await session.commit()
await session.refresh(wf)
return wf
async def get_workflow(self, trace_id: str) -> Optional[Workflow]:
async with self.async_session_maker() as session:
statement = select(Workflow).where(Workflow.trace_id == trace_id)
results = await session.execute(statement)
return results.scalar_one_or_none()
async def update_workflow_status(
self, trace_id: str, status: str
) -> Optional[Workflow]:
async with self.async_session_maker() as session:
statement = select(Workflow).where(Workflow.trace_id == trace_id)
results = await session.execute(statement)
record = results.scalar_one_or_none()
if record:
record.status = status
await session.commit()
await session.refresh(record)
return record
async def list_workflows(self, user_id: str) -> List[Workflow]:
async with self.async_session_maker() as session:
statement = select(Workflow).where(Workflow.user_id == user_id)
results = await session.execute(statement)
return results.scalars().all()
async def upsert_workflow_context(
self, trace_id: str, **kwargs
) -> WorkflowContextModel:
async with self.async_session_maker() as session:
statement = select(WorkflowContextModel).where(
WorkflowContextModel.trace_id == trace_id
)
results = await session.execute(statement)
record = results.scalar_one_or_none()
if record:
for key, value in kwargs.items():
setattr(record, key, value)
else:
record = WorkflowContextModel(trace_id=trace_id, **kwargs)
session.add(record)
await session.commit()
await session.refresh(record)
return record
async def get_workflow_context(
self, trace_id: str
) -> Optional[WorkflowContextModel]:
async with self.async_session_maker() as session:
statement = select(WorkflowContextModel).where(
WorkflowContextModel.trace_id == trace_id
)
results = await session.execute(statement)
return results.scalar_one_or_none()