Spaces:
Build error
Build error
kamaleswar Mohanta
Refactor SdlcNode for improved prompt handling and error logging; streamline user story and design document generation processes.
cbeaa79 | from langgraph.graph import StateGraph, START, END | |
| from src.langgraphagenticai.state.state import SDLCStages, SDLCState as State | |
| from langchain_core.messages import SystemMessage, HumanMessage | |
| import streamlit as st | |
| import json | |
| from datetime import datetime | |
| from typing import List | |
| from src.langgraphagenticai.logging.logging_utils import logger, log_entry_exit | |
| from src.langgraphagenticai.prompt_library import prompt # Assuming your prompts are here | |
| # import json # Duplicate import, already imported | |
| from tenacity import retry, stop_after_attempt, wait_exponential | |
| import functools | |
| import time | |
| import re | |
| class SdlcNode: | |
| def __init__(self, model): | |
| """Initialize the SdlcNode with an LLM.""" | |
| self.llm = model | |
| def user_input(self, state: State) -> dict: | |
| """Handle user input, distinguishing between initial requirements and feedback.""" | |
| logger.info(f"Executing user_input with state: {state}") | |
| if state.get_last_feedback_for_stage(SDLCStages.PLANNING): | |
| state.user_stories = None | |
| state.project_name = st.session_state.get("project_name") | |
| state.project_description = st.session_state.get("project_description") | |
| state.project_goals = st.session_state.get("project_goals") | |
| state.project_scope = st.session_state.get("project_scope") | |
| state.project_objectives = st.session_state.get("project_objectives") | |
| state.feedback = st.session_state.get("feedback", {}) | |
| state.feedback_decision = st.session_state.get("feedback_decision") | |
| state.current_stage = st.session_state.get("current_stage", SDLCStages.PLANNING) | |
| state.generated_requirements = st.session_state.get("generated_requirements") | |
| state.user_stories = st.session_state.get("user_stories") | |
| return {"user_input": "captured"} | |
| def generate_requirements(self, state: State) -> dict: | |
| """Generate requirements based on user input.""" | |
| logger.info(f"Generating requirements with state: {state}") | |
| try: | |
| requirements_input = { | |
| "project_name": state.project_name if state.project_name is not None else "No project name provided", | |
| "project_description": state.project_description if state.project_description is not None else "No project description provided", | |
| "project_goals": state.project_goals if state.project_goals is not None else "No project goals provided", | |
| "project_scope": state.project_scope if state.project_scope is not None else "No project scope provided", | |
| "project_objectives": state.project_objectives if state.project_objectives is not None else "No project objectives provided", | |
| } | |
| requirements_input_json = json.dumps(requirements_input, indent=2) | |
| prompt_string = prompt.REQUIREMENTS_PROMPT_STRING.format(requirements_input=requirements_input_json) | |
| # Assuming REQUIREMENTS_sys_prompt does not need .format() or is formatted elsewhere if needed | |
| sys_prompt_content = prompt.REQUIREMENTS_sys_prompt | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.generated_requirements = response.content if hasattr(response, 'content') else str(response) | |
| return {"generated_requirements": state.generated_requirements} | |
| except Exception as e: | |
| logger.error(f"Error generating requirements: {e}") | |
| state.generated_requirements = f"Error generating requirements: {str(e)}" | |
| return {"generated_requirements": state.generated_requirements} | |
| def generate_user_stories(self, state: State) -> dict: | |
| """Generate user stories based on the requirements.""" | |
| logger.info("Generating user stories") | |
| if not state.generated_requirements: | |
| state.user_stories = "No requirements generated yet." | |
| logger.warning("Cannot generate user stories without requirements.") | |
| return {"user_stories": state.user_stories} | |
| formatted_requirements = str(state.generated_requirements).replace('{', '{{').replace('}', '}}') | |
| feedback = state.get_last_feedback_for_stage(SDLCStages.PLANNING) | |
| logger.info(f"Feedback for user stories: {feedback}") | |
| project_name_val = state.project_name or 'N/A' | |
| project_name_formatted = str(project_name_val).replace('{', '{{').replace('}', '}}') | |
| if feedback: | |
| formatted_feedback = str(feedback).replace('{', '{{').replace('}', '}}') | |
| try: | |
| prompt_string = prompt.USER_STORIES_FEEDBACK_PROMPT_STRING.format( | |
| generated_requirements=formatted_requirements, | |
| feedback=formatted_feedback | |
| ) | |
| sys_prompt_content = prompt.USER_STORIES_FEEDBACK_SYS_PROMPT.format( | |
| generated_requirements=formatted_requirements, | |
| project_name=project_name_formatted, | |
| feedback=formatted_feedback | |
| ) | |
| except KeyError as e: | |
| logger.error(f"KeyError during formatting USER_STORIES_FEEDBACK prompts: {e}") | |
| logger.error(f"Available keys: generated_requirements, project_name, feedback") | |
| state.user_stories = f"Error formatting user story prompt: {e}" | |
| return {"user_stories": state.user_stories} | |
| else: | |
| try: | |
| prompt_string = prompt.USER_STORIES_NO_FEEDBACK_PROMPT_STRING.format( | |
| generated_requirements=formatted_requirements, | |
| project_name=project_name_formatted | |
| ) | |
| sys_prompt_content = prompt.USER_STORIES_NO_FEEDBACK_SYS_PROMPT.format( | |
| generated_requirements=formatted_requirements, | |
| project_name=project_name_formatted | |
| ) | |
| except KeyError as e: | |
| logger.error(f"KeyError during formatting USER_STORIES_NO_FEEDBACK prompts: {e}") | |
| logger.error(f"Available keys: generated_requirements, project_name") | |
| state.user_stories = f"Error formatting user story prompt: {e}" | |
| return {"user_stories": state.user_stories} | |
| try: | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.user_stories = response.content if hasattr(response, 'content') else str(response) | |
| st.session_state["user_stories"] = state.user_stories # Update session state | |
| logger.info(f"--- RAW state.user_stories after generation ---") | |
| logger.info(state.user_stories) | |
| logger.info(f"--- END RAW state.user_stories ---") | |
| return {"user_stories": state.user_stories} | |
| except Exception as e: | |
| logger.error(f"Error generating user stories: {e}") | |
| state.user_stories = f"Error generating user stories: {str(e)}" | |
| return {"user_stories": state.user_stories} | |
| def design_documents(self, state: State) -> dict[str, str]: | |
| """Generate design documents based on user stories with robust validation.""" | |
| state.feedback_decision = None | |
| logger.info("Generating design documents") | |
| if not state.user_stories or not str(state.user_stories).strip(): | |
| state.design_documents = "No user stories provided for design document generation." | |
| logger.warning("Cannot generate design documents without user stories.") | |
| return {"design_documents": state.design_documents} | |
| user_stories_for_prompt = str(state.user_stories).replace('{', '{{').replace('}', '}}') | |
| project_name_val = state.project_name or 'N/A' | |
| project_name_for_prompt = str(project_name_val).replace('{', '{{').replace('}', '}}') | |
| logger.info(f"--- User Stories for TDD Prompt (escaped) ---") | |
| logger.info(user_stories_for_prompt[:1000] + "..." if len(user_stories_for_prompt) > 1000 else user_stories_for_prompt) # Log a snippet | |
| logger.info(f"--- END User Stories for TDD Prompt ---") | |
| # Get feedback for design stage (currently not used in these prompts but good to have) | |
| # feedback = state.get_last_feedback_for_stage(SDLCStages.DESIGN) | |
| # logger.info(f"Feedback for design documents: {feedback or 'None'}") | |
| try: | |
| # Use the detailed prompts from prompt_library | |
| # Ensure the placeholders in your prompts match what you provide here. | |
| # The prompts DESIGN_DOCUMENTS_NO_FEEDBACK_... expect {project_name} and {user_stories} | |
| sys_prompt_content = prompt.DESIGN_DOCUMENTS_NO_FEEDBACK_SYS_PROMPT.format( | |
| user_stories=user_stories_for_prompt, | |
| project_name=project_name_for_prompt | |
| ) | |
| prompt_string_content = prompt.DESIGN_DOCUMENTS_NO_FEEDBACK_PROMPT_STRING.format( | |
| user_stories=user_stories_for_prompt, | |
| project_name=project_name_for_prompt # Assuming this is also needed here | |
| ) | |
| logger.debug(f"--- Formatted System Prompt for TDD (first 500 chars) ---") | |
| logger.debug(sys_prompt_content[:500] + "...") | |
| logger.debug(f"--- Formatted Human Prompt for TDD (first 500 chars) ---") | |
| logger.debug(prompt_string_content[:500] + "...") | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string_content) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.design_documents = response.content if hasattr(response, 'content') else str(response) | |
| logger.info("Design documents generated successfully.") | |
| return {"design_documents": state.design_documents} | |
| except KeyError as ke: | |
| error_msg = f"KeyError during TDD prompt formatting: {str(ke)}. This means a placeholder in your DESIGN_DOCUMENTS_... prompts was not found in the .format() call." | |
| logger.error(error_msg) | |
| logger.error(f"User stories (snippet): {user_stories_for_prompt[:200]}") | |
| logger.error(f"Project name: {project_name_for_prompt}") | |
| state.design_documents = error_msg | |
| return {"design_documents": state.design_documents} | |
| except Exception as e: | |
| error_msg = f"Error generating design documents: {type(e).__name__} - {str(e)}" | |
| state.design_documents = error_msg | |
| logger.error(f"{error_msg}, Input User Stories (snippet): {user_stories_for_prompt[:200]}...") | |
| return {"design_documents": state.design_documents} | |
| def development_artifact(self, state: State) -> dict: | |
| """Generate development artifacts based on design documents.""" | |
| logger.info("Generating development artifacts") | |
| if not state.design_documents: | |
| state.development_artifact = "No design documents generated yet." | |
| logger.warning("Cannot generate development artifacts without design documents.") | |
| return {"development_artifact": state.development_artifact} | |
| # Escape design_documents and project_name for .format() | |
| design_documents_for_prompt = str(state.design_documents).replace('{', '{{').replace('}', '}}') | |
| project_name_val = state.project_name or 'N/A' | |
| project_name_for_prompt = str(project_name_val).replace('{', '{{').replace('}', '}}') | |
| try: | |
| prompt_string = prompt.DEVELOPMENT_ARTIFACT_PROMPT_STRING.format( | |
| design_documents=design_documents_for_prompt | |
| ) | |
| sys_prompt_content = prompt.DEVELOPMENT_ARTIFACT_SYS_PROMPT.format( | |
| project_name=project_name_for_prompt | |
| ) | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.development_artifact = response.content if hasattr(response, 'content') else str(response) | |
| return {"development_artifact": state.development_artifact} | |
| except KeyError as ke: | |
| error_msg = f"KeyError during DEVELOPMENT_ARTIFACT prompt formatting: {str(ke)}." | |
| logger.error(error_msg) | |
| state.development_artifact = error_msg | |
| return {"development_artifact": state.development_artifact} | |
| except Exception as e: | |
| logger.error(f"Error generating development artifacts: {e}") | |
| state.development_artifact = f"Error generating development artifacts: {str(e)}" | |
| return {"development_artifact": state.development_artifact} | |
| def testing_artifact(self, state: State) -> dict: | |
| """Generate testing artifacts based on development artifacts.""" | |
| logger.info("Generating testing artifacts") | |
| if not state.development_artifact: | |
| state.testing_artifact = "No development artifacts generated yet." | |
| logger.warning("Cannot generate testing artifacts without development artifacts.") | |
| return {"testing_artifact": state.testing_artifact} | |
| # Escape inputs for .format() | |
| user_stories_for_prompt = str(state.user_stories).replace('{', '{{').replace('}', '}}') | |
| development_artifact_for_prompt = str(state.development_artifact).replace('{', '{{').replace('}', '}}') | |
| project_name_val = state.project_name or 'N/A' | |
| project_name_for_prompt = str(project_name_val).replace('{', '{{').replace('}', '}}') | |
| try: | |
| prompt_string = prompt.TESTING_ARTIFACT_PROMPT_STRING.format( | |
| user_stories=user_stories_for_prompt, | |
| development_artifact=development_artifact_for_prompt | |
| ) | |
| sys_prompt_content = prompt.TESTING_ARTIFACT_SYS_PROMPT.format( | |
| project_name=project_name_for_prompt | |
| ) | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.testing_artifact = response.content if hasattr(response, 'content') else str(response) | |
| return {"testing_artifact": state.testing_artifact} | |
| except KeyError as ke: | |
| error_msg = f"KeyError during TESTING_ARTIFACT prompt formatting: {str(ke)}." | |
| logger.error(error_msg) | |
| state.testing_artifact = error_msg | |
| return {"testing_artifact": state.testing_artifact} | |
| except Exception as e: | |
| logger.error(f"Error generating testing artifacts: {e}") | |
| state.testing_artifact = f"Error generating testing artifacts: {str(e)}" | |
| return {"testing_artifact": state.testing_artifact} | |
| def deployment_artifact(self, state: State) -> dict: | |
| """Generate deployment artifacts based on testing artifacts.""" | |
| logger.info("Generating deployment artifacts") | |
| if not state.testing_artifact: | |
| state.deployment_artifact = "No testing artifacts generated yet." | |
| logger.warning("Cannot generate deployment artifacts without testing artifacts.") | |
| return {"deployment_artifact": state.deployment_artifact} | |
| # Escape inputs for .format() | |
| # The DEPLOYMENT_ARTIFACT_PROMPT_STRING uses {state}, which is not standard for .format() | |
| # It should be specific fields like {state.testing_artifact} or {testing_artifact} | |
| # For now, assuming it wants the string representation of the testing_artifact and project_name | |
| testing_artifact_for_prompt = str(state.testing_artifact).replace('{', '{{').replace('}', '}}') | |
| project_name_val = state.project_name or 'N/A' | |
| project_name_for_prompt = str(project_name_val).replace('{', '{{').replace('}', '}}') | |
| try: | |
| # --- IMPORTANT: Review prompt.DEPLOYMENT_ARTIFACT_PROMPT_STRING --- | |
| # It currently has .format(state=state). This is unusual. | |
| # It should likely be .format(testing_artifact=testing_artifact_for_prompt, project_name=project_name_for_prompt) | |
| # or similar, depending on its actual placeholders. | |
| # For now, I'll assume it expects 'testing_artifact' and 'project_name' as keys. | |
| # You MUST adjust this if your prompt uses different placeholder names. | |
| # Tentative formatting based on common patterns, ADJUST IF YOUR PROMPT IS DIFFERENT | |
| try: | |
| prompt_string_content = prompt.DEPLOYMENT_ARTIFACT_PROMPT_STRING.format( | |
| testing_artifact=testing_artifact_for_prompt | |
| # Add other fields from 'state' if your prompt actually uses them like {state.project_name} | |
| ) | |
| # Assuming DEPLOYMENT_ARTIFACT_SYS_PROMPT expects project_name | |
| sys_prompt_content = prompt.DEPLOYMENT_ARTIFACT_SYS_PROMPT.format( | |
| project_name=project_name_for_prompt | |
| ) | |
| except KeyError as ke: | |
| # Fallback if the prompt string uses {state.testing_artifact} directly (less common for general prompts) | |
| if 'state.testing_artifact' in str(ke): # Check if the error is about 'state.testing_artifact' | |
| prompt_string_content = prompt.DEPLOYMENT_ARTIFACT_PROMPT_STRING.format( | |
| state=state # Pass the whole state object if the prompt needs it this way | |
| ) | |
| sys_prompt_content = prompt.DEPLOYMENT_ARTIFACT_SYS_PROMPT.format( | |
| project_name=project_name_for_prompt # This part is likely fine | |
| ) | |
| else: # Re-raise if it's a different KeyError | |
| raise ke | |
| messages = [ | |
| SystemMessage(content=sys_prompt_content), | |
| HumanMessage(content=prompt_string_content) | |
| ] | |
| response = self.llm.invoke(messages) | |
| state.deployment_artifact = response.content if hasattr(response, 'content') else str(response) | |
| return {"deployment_artifact": state.deployment_artifact} | |
| except KeyError as ke: | |
| error_msg = f"KeyError during DEPLOYMENT_ARTIFACT prompt formatting: {str(ke)}. Review prompt placeholders." | |
| logger.error(error_msg) | |
| logger.error(f"Prompt string might be: {prompt.DEPLOYMENT_ARTIFACT_PROMPT_STRING}") | |
| state.deployment_artifact = error_msg | |
| return {"deployment_artifact": state.deployment_artifact} | |
| except Exception as e: | |
| logger.error(f"Error generating deployment artifacts: {e}") | |
| state.deployment_artifact = f"Error generating deployment artifacts: {str(e)}" | |
| return {"deployment_artifact": state.deployment_artifact} | |
| def process_feedback(self, state: State) -> dict: | |
| """ | |
| Process user feedback and update state with decision. | |
| """ | |
| logger.info(f"Processing feedback. Current feedback state: {state.feedback}") | |
| current_stage = state.current_stage | |
| # feedback_decision is set by the UI/controller before calling this node | |
| feedback_text_from_ui = state.feedback.get(current_stage.value, [None])[-1] # Get latest feedback for current stage | |
| logger.info(f"Processing feedback for stage: {current_stage}, Decision from UI: {state.feedback_decision}, Text: {feedback_text_from_ui}") | |
| if state.feedback_decision == "accept": | |
| logger.info(f"Feedback for stage '{current_stage}' is ACCEPT based on UI decision.") | |
| # Optionally, store "accept" as a feedback entry if needed for history, though decision is primary | |
| state.add_feedback(current_stage, "User accepted.") | |
| elif state.feedback_decision == "reject": | |
| logger.info(f"Feedback for stage '{current_stage}' is REJECT based on UI decision. Feedback text: {feedback_text_from_ui}") | |
| if feedback_text_from_ui: | |
| state.add_feedback(current_stage, str(feedback_text_from_ui)) # Add the actual feedback text | |
| else: | |
| state.add_feedback(current_stage, "User rejected, no specific feedback provided.") | |
| else: # Should not happen if UI sets feedback_decision correctly | |
| logger.warning(f"Unknown feedback_decision '{state.feedback_decision}' for stage {current_stage}. Defaulting to reject.") | |
| state.feedback_decision = "reject" | |
| state.add_feedback(current_stage, f"System default to reject due to unknown decision: {state.feedback_decision}") | |
| logger.info(f"Updated feedback state: {state.feedback}") | |
| return {"feedback_decision": state.feedback_decision} | |
| def feedback_route(self, state: State) -> str: | |
| """Routes based on the feedback decision stored in the state.""" | |
| logger.info(f"Entering feedback_route with decision: {state.feedback_decision}") | |
| if not isinstance(state, State): | |
| logger.error(f"Invalid state type: {type(state)}. Routing to reject.") | |
| return "reject" # Or handle as an error state | |
| decision = state.feedback_decision # Already processed in process_feedback | |
| if decision == "accept": | |
| logger.info("Feedback accepted. Routing to next stage.") | |
| next_stage = state.get_next_stage() | |
| if next_stage: | |
| state.update_stage(next_stage) # This should update current_stage | |
| st.session_state["current_stage"] = state.current_stage # Reflect in Streamlit | |
| logger.info(f"Updated state to next stage: {state.current_stage}") | |
| else: | |
| logger.info("No next stage available. Ending process.") | |
| # No need to update current_stage if ending | |
| state.clear_feedback_decision() # Clear for the next cycle | |
| st.session_state["feedback_decision"] = None # Reflect in Streamlit | |
| # Clear specific feedback text from UI for the accepted stage if desired | |
| # st.session_state["feedback"] = {} # Or selectively clear | |
| logger.info(f"Feedback decision cleared. Current state feedback: {state.feedback}") | |
| return "accept" | |
| else: # Covers 'reject' and any other case defaulting to reject | |
| logger.info(f"Feedback decision is '{decision}'. Routing back for revision of stage {state.current_stage}.") | |
| # The current stage remains the same for revision. | |
| state.clear_feedback_decision() | |
| st.session_state["feedback_decision"] = None | |
| logger.info(f"Feedback decision cleared for revision. Current state feedback: {state.feedback}") | |
| return "reject" |