Skip to content

Commit 59b9102

Browse files
author
Zvi Fried
committed
complete workflow more testing needed
1 parent ae51d72 commit 59b9102

23 files changed

+3672
-195
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""
2+
Coding task manager for enhanced MCP as a Judge workflow v3.
3+
4+
This module provides helper functions for managing coding tasks,
5+
including creation, updates, state transitions, and persistence.
6+
"""
7+
8+
import json
9+
import logging
10+
from datetime import datetime
11+
from typing import Optional
12+
13+
from mcp_as_a_judge.db.conversation_history_service import ConversationHistoryService
14+
from mcp_as_a_judge.models.task_metadata import TaskMetadata, TaskState
15+
16+
# Set up logger directly to avoid circular imports
17+
logger = logging.getLogger(__name__)
18+
19+
20+
async def create_new_coding_task(
21+
user_request: str,
22+
task_name: str,
23+
task_title: str,
24+
task_description: str,
25+
user_requirements: str,
26+
tags: list[str],
27+
conversation_service: ConversationHistoryService,
28+
) -> TaskMetadata:
29+
"""
30+
Create a new coding task with auto-generated task_id.
31+
32+
Args:
33+
user_request: Original user request
34+
task_name: Human-readable task name
35+
task_title: Display title
36+
task_description: Detailed description
37+
user_requirements: Initial requirements
38+
tags: Task tags
39+
conversation_service: Conversation service
40+
41+
Returns:
42+
New TaskMetadata instance
43+
"""
44+
logger.info(f"📝 Creating new coding task: {task_title}")
45+
46+
# Create new TaskMetadata with auto-generated UUID
47+
task_metadata = TaskMetadata(
48+
name=task_name,
49+
title=task_title,
50+
description=task_description,
51+
user_requirements=user_requirements,
52+
state=TaskState.CREATED, # Default state for new tasks
53+
tags=tags,
54+
)
55+
56+
# Add initial requirements to history if provided
57+
if user_requirements:
58+
task_metadata.update_requirements(user_requirements, source="initial")
59+
60+
logger.info(f"✅ Created new task metadata: {task_metadata.task_id}")
61+
return task_metadata
62+
63+
64+
async def update_existing_coding_task(
65+
task_id: str,
66+
user_request: str,
67+
task_name: str,
68+
task_title: str,
69+
task_description: str,
70+
user_requirements: Optional[str],
71+
state: Optional[TaskState],
72+
tags: list[str],
73+
conversation_service: ConversationHistoryService,
74+
) -> TaskMetadata:
75+
"""
76+
Update an existing coding task.
77+
78+
Args:
79+
task_id: Immutable task ID
80+
user_request: Original user request
81+
task_name: Updated task name
82+
task_title: Updated title
83+
task_description: Updated description
84+
user_requirements: Updated requirements
85+
state: Updated state
86+
tags: Updated tags
87+
conversation_service: Conversation service
88+
89+
Returns:
90+
Updated TaskMetadata instance
91+
92+
Raises:
93+
ValueError: If task not found or invalid state transition
94+
"""
95+
logger.info(f"📝 Updating existing coding task: {task_id}")
96+
97+
# Load existing task metadata from conversation history
98+
existing_metadata = await load_task_metadata_from_history(
99+
task_id=task_id,
100+
conversation_service=conversation_service,
101+
)
102+
103+
if not existing_metadata:
104+
raise ValueError(f"Task not found: {task_id}")
105+
106+
# Update mutable fields
107+
existing_metadata.name = task_name
108+
existing_metadata.title = task_title
109+
existing_metadata.description = task_description
110+
existing_metadata.tags = tags
111+
existing_metadata.updated_at = datetime.now()
112+
113+
# Update requirements if provided
114+
if user_requirements is not None:
115+
existing_metadata.update_requirements(user_requirements, source="update")
116+
117+
# Update state if provided (with validation)
118+
if state is not None:
119+
validate_state_transition(existing_metadata.state, state)
120+
existing_metadata.update_state(state)
121+
122+
logger.info(f"✅ Updated task metadata: {task_id}")
123+
return existing_metadata
124+
125+
126+
async def load_task_metadata_from_history(
127+
task_id: str,
128+
conversation_service: ConversationHistoryService,
129+
) -> Optional[TaskMetadata]:
130+
"""
131+
Load TaskMetadata from conversation history using task_id as primary key.
132+
133+
Args:
134+
task_id: Task ID to load
135+
conversation_service: Conversation service
136+
137+
Returns:
138+
TaskMetadata if found, None otherwise
139+
"""
140+
try:
141+
# Use task_id as primary key for conversation history
142+
conversation_history = await conversation_service.get_conversation_history(
143+
session_id=task_id
144+
)
145+
146+
# Look for the most recent task metadata record
147+
for record in reversed(conversation_history):
148+
if record.source == "set_coding_task" and "task_metadata" in record.output:
149+
# Parse the task metadata from the record
150+
output_data = json.loads(record.output)
151+
if "current_task_metadata" in output_data:
152+
metadata_dict = output_data["current_task_metadata"]
153+
return TaskMetadata.model_validate(metadata_dict)
154+
155+
return None
156+
157+
except Exception as e:
158+
logger.warning(f"⚠️ Failed to load task metadata from history: {e}")
159+
return None
160+
161+
162+
async def save_task_metadata_to_history(
163+
task_metadata: TaskMetadata,
164+
user_request: str,
165+
action: str,
166+
conversation_service: ConversationHistoryService,
167+
) -> None:
168+
"""
169+
Save TaskMetadata to conversation history using task_id as primary key.
170+
171+
Args:
172+
task_metadata: Task metadata to save
173+
user_request: Original user request
174+
action: Action taken ("created" or "updated")
175+
conversation_service: Conversation service
176+
"""
177+
try:
178+
# Use task_id as primary key for conversation history
179+
await conversation_service.save_tool_interaction(
180+
session_id=task_metadata.task_id,
181+
tool_name="set_coding_task",
182+
tool_input=user_request,
183+
tool_output=json.dumps({
184+
"action": action,
185+
"current_task_metadata": task_metadata.model_dump(mode='json'),
186+
"timestamp": datetime.now().isoformat(),
187+
}),
188+
)
189+
190+
logger.info(f"💾 Saved task metadata to conversation history: {task_metadata.task_id}")
191+
192+
except Exception as e:
193+
logger.error(f"❌ Failed to save task metadata to history: {e}")
194+
# Don't raise - this is not critical for tool operation
195+
196+
197+
def validate_state_transition(current_state: TaskState, new_state: TaskState) -> None:
198+
"""
199+
Validate that the state transition is allowed.
200+
201+
Args:
202+
current_state: Current TaskState
203+
new_state: Requested new TaskState
204+
205+
Raises:
206+
ValueError: If transition is not allowed
207+
"""
208+
# Define valid state transitions
209+
valid_transitions = {
210+
TaskState.CREATED: [TaskState.PLANNING, TaskState.BLOCKED, TaskState.CANCELLED],
211+
TaskState.PLANNING: [TaskState.PLAN_APPROVED, TaskState.CREATED, TaskState.BLOCKED, TaskState.CANCELLED],
212+
TaskState.PLAN_APPROVED: [TaskState.IMPLEMENTING, TaskState.PLANNING, TaskState.BLOCKED, TaskState.CANCELLED],
213+
TaskState.IMPLEMENTING: [TaskState.IMPLEMENTING, TaskState.REVIEW_READY, TaskState.PLAN_APPROVED, TaskState.BLOCKED, TaskState.CANCELLED],
214+
TaskState.REVIEW_READY: [TaskState.COMPLETED, TaskState.IMPLEMENTING, TaskState.BLOCKED, TaskState.CANCELLED],
215+
TaskState.COMPLETED: [TaskState.CANCELLED], # Only allow cancellation of completed tasks
216+
TaskState.BLOCKED: [TaskState.CREATED, TaskState.PLANNING, TaskState.PLAN_APPROVED, TaskState.IMPLEMENTING, TaskState.REVIEW_READY, TaskState.CANCELLED],
217+
TaskState.CANCELLED: [], # No transitions from cancelled state
218+
}
219+
220+
if new_state not in valid_transitions.get(current_state, []):
221+
raise ValueError(
222+
f"Invalid state transition: {current_state.value}{new_state.value}. "
223+
f"Valid transitions from {current_state.value}: {[s.value for s in valid_transitions.get(current_state, [])]}"
224+
)

src/mcp_as_a_judge/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
# LLM Configuration
8-
MAX_TOKENS = 10000 # Maximum tokens for all LLM requests
8+
MAX_TOKENS = 25000 # Maximum tokens for all LLM requests - increased for comprehensive responses
99
DEFAULT_TEMPERATURE = 0.1 # Default temperature for LLM requests
1010

1111
# Timeout Configuration

src/mcp_as_a_judge/messaging/factory.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
This module implements the factory pattern for creating the appropriate
55
messaging provider based on capabilities and preferences. It automatically
66
detects MCP sampling capability and selects the best available provider.
7+
8+
The factory handles ALL message format decisions to ensure consistency.
79
"""
810

11+
from typing import Any
912
from mcp.server.fastmcp import Context
1013

1114
from mcp_as_a_judge.messaging.interface import MessagingConfig, MessagingProvider
1215
from mcp_as_a_judge.messaging.llm_api_provider import LLMAPIProvider
1316
from mcp_as_a_judge.messaging.mcp_sampling_provider import MCPSamplingProvider
17+
from mcp_as_a_judge.messaging.converters import mcp_messages_to_universal, validate_message_conversion
1418

1519

1620
class MessagingProviderFactory:
@@ -62,6 +66,66 @@ def create_provider(ctx: Context, config: MessagingConfig) -> MessagingProvider:
6266
f"MCP sampling: {sampling_available}, LLM API: {llm_available}"
6367
)
6468

69+
@staticmethod
70+
def get_provider_with_messages(
71+
ctx: Context,
72+
messages: list[Any],
73+
config: MessagingConfig
74+
) -> tuple[MessagingProvider, list[Any]]:
75+
"""
76+
Get the appropriate provider and convert messages to the correct format.
77+
78+
This method makes ALL message format decisions at the factory level
79+
to ensure consistency throughout the system.
80+
81+
Args:
82+
ctx: MCP context for capability detection
83+
messages: Input messages (typically SamplingMessage objects)
84+
config: Messaging configuration
85+
86+
Returns:
87+
Tuple of (provider, formatted_messages) where formatted_messages
88+
are in the correct format for the selected provider
89+
"""
90+
# Check provider availability
91+
sampling_provider = MCPSamplingProvider(ctx)
92+
llm_provider = LLMAPIProvider()
93+
94+
sampling_available = sampling_provider.is_available()
95+
llm_available = llm_provider.is_available()
96+
97+
if config.prefer_sampling:
98+
# Prefer sampling first, fall back to LLM API
99+
if sampling_available:
100+
# MCP sampling expects SamplingMessage objects directly
101+
return sampling_provider, messages
102+
elif llm_available:
103+
# LLM API expects universal Message objects
104+
universal_messages = mcp_messages_to_universal(messages)
105+
106+
# Validate conversion
107+
if not validate_message_conversion(messages, universal_messages):
108+
raise ValueError("Failed to convert messages to universal format")
109+
110+
return llm_provider, universal_messages
111+
else:
112+
# Force LLM API only (no fallback to sampling)
113+
if llm_available:
114+
# LLM API expects universal Message objects
115+
universal_messages = mcp_messages_to_universal(messages)
116+
117+
# Validate conversion
118+
if not validate_message_conversion(messages, universal_messages):
119+
raise ValueError("Failed to convert messages to universal format")
120+
121+
return llm_provider, universal_messages
122+
123+
# No providers available
124+
raise RuntimeError(
125+
f"No messaging providers available. "
126+
f"MCP sampling: {sampling_available}, LLM API: {llm_available}"
127+
)
128+
65129
@staticmethod
66130
def check_sampling_capability(ctx: Context) -> bool:
67131
"""Check if the context has sampling capability enabled.

0 commit comments

Comments
 (0)