From 811ed72615cba2f5830c643a7af43318888f657a Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 23 Apr 2025 09:01:52 +0200 Subject: [PATCH] basic workflow running --- app.py | 3 +- config.ini | 1 + connectors/connector_aichat_openai.py | 1 - modules/chat.py | 87 ++++++----- modules/chat_agent_analyst.py | 27 ++-- modules/chat_agent_coder.py | 204 +++++++++++++++++++++----- modules/chat_agent_documentation.py | 24 +-- modules/chat_agent_webcrawler.py | 14 +- modules/chat_registry.py | 20 +-- modules/lucydom_interface.py | 44 +++--- modules/lucydom_model.py | 24 ++- notes/changelog.txt | 6 +- static/1_test_document.txt | 10 -- static/2_test_image.png | Bin 284 -> 0 bytes static/3_generated_code.py | 52 ------- static/4_execution_error.txt | 6 - test2.py | 50 ------- test_workflow1.py | 4 +- testcode.py | 66 +++++++++ 19 files changed, 372 insertions(+), 271 deletions(-) delete mode 100644 static/1_test_document.txt delete mode 100644 static/2_test_image.png delete mode 100644 static/3_generated_code.py delete mode 100644 static/4_execution_error.txt delete mode 100644 test2.py create mode 100644 testcode.py diff --git a/app.py b/app.py index 092bc199..ff2c5eb8 100644 --- a/app.py +++ b/app.py @@ -28,7 +28,8 @@ from modules.auth import ( # Import models - import generically for INITIALIZATION, even if dummy! import modules.gateway_model as gateway_model -import modules.lucydom_interface as lucydom_model + +#from modules.lucydom_interface import get_lucydom_interface as dom_interface def init_logging(): diff --git a/config.ini b/config.ini index 2cbce637..55ec0361 100644 --- a/config.ini +++ b/config.ini @@ -52,3 +52,4 @@ Agent_Webcrawler_MAX_SEARCH_RESULTS = 5 # Agent Coder configuration Agent_Coder_INSTALL_TIMEOUT = 180 Agent_Coder_EXECUTION_TIMEOUT = 60 +Agent_Coder_EXECUTION_RETRY = 5 diff --git a/connectors/connector_aichat_openai.py b/connectors/connector_aichat_openai.py index 456490a6..7de51e67 100644 --- a/connectors/connector_aichat_openai.py +++ b/connectors/connector_aichat_openai.py @@ -37,7 +37,6 @@ class ChatService: "Content-Type": "application/json" } ) - logger.info(f"OpenAI Connector initialized with model: {self.model_name}") async def call_api(self, messages: List[Dict[str, Any]], temperature: float = None, max_tokens: int = None) -> str: diff --git a/modules/chat.py b/modules/chat.py index 442341f6..2cc973c9 100644 --- a/modules/chat.py +++ b/modules/chat.py @@ -14,14 +14,25 @@ from datetime import datetime from typing import Dict, Any, List, Optional, Union # Required imports -from connectors.connector_aichat_openai import ChatService from modules.chat_registry import get_agent_registry -from modules.lucydom_interface import get_lucydom_interface, GLOBAL_SETTINGS +from modules.lucydom_interface import get_lucydom_interface as dom_interface from modules.chat_content_extraction import get_document_contents # Configure logger logger = logging.getLogger(__name__) +# Global settings for the workflow management +GLOBAL_WorkflowLabels = { + "system_name": "AI Assistant", # Default system name for logs + "workflow_status_messages": { + "init": "Workflow initialized", + "running": "Running workflow", + "waiting": "Waiting for input", + "completed": "Workflow completed", + "error": "Error in workflow" + } +} + class ChatManager: """ Manages the processing of chat requests, agent execution, and @@ -38,13 +49,9 @@ class ChatManager: """ self.mandate_id = mandate_id self.user_id = user_id - self.ai_service = ChatService() - self.lucy_interface = get_lucydom_interface(mandate_id, user_id) + self.mydom = dom_interface(mandate_id, user_id) self.agent_registry = get_agent_registry() - self.agent_registry.set_ai_service(self.ai_service) - - # Set AI service in lucy interface for language support - self.lucy_interface.set_ai_service(self.ai_service) + self.agent_registry.set_mydom(self.mydom) ### Chat Management @@ -73,9 +80,9 @@ class ChatManager: obj_workplan = project_manager_response.get("obj_workplan", []) obj_user_response = project_manager_response.get("obj_user_response", "") - # Get detected language and set it in the lucy interface + # Get detected language and set it in the mydom interface user_language = project_manager_response.get("user_language", "en") - self.lucy_interface.set_user_language(user_language) + self.mydom.set_user_language(user_language) # 4. Save the response as a message in the workflow and add log entries response_message = { @@ -203,7 +210,7 @@ JSON_OUTPUT = {{ }} # Multiple agent tasks can be added here and should build logically on each other ], - "obj_user_response": "Information to the user about how his request will be solved.", + "obj_user_response": "Information to the user about how his request will be solved, in the language of the user's request.", "user_language": "en" # Language code (e.g., en, de, fr, es) based on the user's request }} @@ -227,9 +234,9 @@ JSON_OUTPUT = {{ 4. If you use label for an existing file """ - # Call the AI service through lucy_interface for language support + # Call the AI service through mydom for language support logger.debug(f"Planning prompt: {prompt}") - project_manager_output = await self.lucy_interface.call_ai([ + project_manager_output = await self.mydom.call_ai([ { "role": "system", "content": "You are an experienced project manager who analyzes user requests and creates work plans. You pay very careful attention to ensure that all document dependencies are correct and that no non-existent documents are defined as inputs. The output follows strictly the specified format." @@ -318,8 +325,8 @@ JSON_OUTPUT = {{ matching_documents.append(doc_ref) break - # Use the lucy_interface for language-aware AI calls - final_prompt = await self.lucy_interface.call_ai([ + # Use the mydom for language-aware AI calls + final_prompt = await self.mydom.call_ai([ {"role": "system", "content": "You are a project manager, who delivers results to a user."}, {"role": "user", "content": f""" Give the final short feedback to the user with reference to the initial statement (obj_user_response). Provide a list of delivered files (files_delivered). If in the list of delivered files (files_delivered) some files from the original list (files_promised) are not available, then just give a comment on this, otherwise task is completed. @@ -359,7 +366,7 @@ JSON_OUTPUT = {{ """ current_time = datetime.now().isoformat() - if workflow_id is None or not self.lucy_interface.get_workflow(workflow_id): + if workflow_id is None or not self.mydom.get_workflow(workflow_id): # Create new workflow new_workflow_id = str(uuid.uuid4()) if workflow_id is None else workflow_id workflow = { @@ -390,13 +397,13 @@ JSON_OUTPUT = {{ "last_activity": workflow["last_activity"], "message_ids": workflow["message_ids"] # Include message_ids } - self.lucy_interface.create_workflow(workflow_db) + self.mydom.create_workflow(workflow_db) - self.log_add(workflow, GLOBAL_SETTINGS["workflow_status_messages"]["init"], level="info", progress=0) + self.log_add(workflow, GLOBAL_WorkflowLabels["workflow_status_messages"]["init"], level="info", progress=0) return workflow else: # Load existing workflow - workflow = self.lucy_interface.load_workflow_state(workflow_id) + workflow = self.mydom.load_workflow_state(workflow_id) # Ensure message_ids exists if "message_ids" not in workflow: @@ -404,7 +411,7 @@ JSON_OUTPUT = {{ workflow["message_ids"] = [msg["id"] for msg in workflow.get("messages", [])] # Update in database - self.lucy_interface.update_workflow(workflow_id, {"message_ids": workflow["message_ids"]}) + self.mydom.update_workflow(workflow_id, {"message_ids": workflow["message_ids"]}) # Update status and increment round counter workflow["status"] = "running" @@ -422,9 +429,9 @@ JSON_OUTPUT = {{ "last_activity": workflow["last_activity"], "current_round": workflow["current_round"] } - self.lucy_interface.update_workflow(workflow_id, workflow_update) + self.mydom.update_workflow(workflow_id, workflow_update) - self.log_add(workflow, GLOBAL_SETTINGS["workflow_status_messages"]["running"], level="info", progress=0) + self.log_add(workflow, GLOBAL_WorkflowLabels["workflow_status_messages"]["running"], level="info", progress=0) return workflow def workflow_finish(self, workflow: Dict[str, Any]) -> Dict[str, Any]: @@ -448,9 +455,9 @@ JSON_OUTPUT = {{ workflow["last_activity"] = workflow_update["last_activity"] # Save workflow state to database - only relevant fields, not the messages list - self.lucy_interface.update_workflow(workflow["id"], workflow_update) + self.mydom.update_workflow(workflow["id"], workflow_update) - self.log_add(workflow, GLOBAL_SETTINGS["workflow_status_messages"]["completed"], level="info", progress=100) + self.log_add(workflow, GLOBAL_WorkflowLabels["workflow_status_messages"]["completed"], level="info", progress=100) return workflow async def workflow_summarize(self, workflow: Dict[str, Any], message_user: Dict[str, Any]) -> str: @@ -583,8 +590,8 @@ JSON_OUTPUT = {{ # Extract and provide only the relevant information as requested. """ - # Call the AI service through lucy_interface for language support - processed_data = await self.lucy_interface.call_ai([ + # Call the AI service through mydom for language support + processed_data = await self.mydom.call_ai([ {"role": "system", "content": "You are a document processing assistant. Extract only the relevant information as requested."}, {"role": "user", "content": ai_prompt} ]) @@ -660,7 +667,7 @@ JSON_OUTPUT = {{ "workflow_round": workflow.get("current_round", 1), "agent_type": agent_name, "timestamp": datetime.now().isoformat(), - "language": self.lucy_interface.user_language # Pass language to agent + "language": self.mydom.user_language # Pass language to agent } } @@ -668,7 +675,7 @@ JSON_OUTPUT = {{ try: # Process the task using the agent's standardized interface logger.debug("TASK: "+self.parse_json2text(agent_task)) - logger.debug(f"Agent '{agent_name}' AI service available: {agent.ai_service is not None}") + logger.debug(f"Agent '{agent_name}' AI service available: {agent.mydom is not None}") agent_results = await agent.process_task(agent_task) @@ -753,7 +760,7 @@ JSON_OUTPUT = {{ file_content = content # Save file to database - file_meta = self.lucy_interface.save_uploaded_file(file_content, label) + file_meta = self.mydom.save_uploaded_file(file_content, label) if file_meta and "id" in file_meta: file_id = file_meta["id"] @@ -827,14 +834,14 @@ JSON_OUTPUT = {{ workflow["last_activity"] = current_time # Save to database - first the message itself - self.lucy_interface.create_workflow_message(message) + self.mydom.create_workflow_message(message) # Then save the workflow with updated references workflow_update = { "last_activity": current_time, "message_ids": workflow["message_ids"] # Update the message_ids field } - self.lucy_interface.update_workflow(workflow["id"], workflow_update) + self.mydom.update_workflow(workflow["id"], workflow_update) return message @@ -853,8 +860,8 @@ JSON_OUTPUT = {{ content = message.get("content", "") try: - # Use the lucy_interface for language-aware AI calls - content_summary = await self.lucy_interface.call_ai([ + # Use the mydom for language-aware AI calls + content_summary = await self.mydom.call_ai([ {"role": "system", "content": f"You are a chat message summarizer. Create a very concise summary (2-3 sentences, maximum 300 characters)"}, {"role": "user", "content": content} ]) @@ -890,8 +897,8 @@ JSON_OUTPUT = {{ is_text = content.get("metadata", {}).get("is_text", False) try: - # Use the lucy_interface for language-aware AI calls - summary = await self.lucy_interface.call_ai([ + # Use the mydom for language-aware AI calls + summary = await self.mydom.call_ai([ {"role": "system", "content": "You are a content summarizer. Create very concise summary (1-2 sentences, maximum 200 characters) about this file."}, {"role": "user", "content": f"Summarize this {content_type} content briefly:\n\n{data}"} ]) @@ -921,7 +928,7 @@ JSON_OUTPUT = {{ for file_id in file_ids: try: # Check if the file exists - file = self.lucy_interface.get_file(file_id) + file = self.mydom.get_file(file_id) if not file: logger.warning(f"File with ID {file_id} not found") continue @@ -932,7 +939,7 @@ JSON_OUTPUT = {{ continue # Load file content - file_content = self.lucy_interface.get_file_data(file_id) + file_content = self.mydom.get_file_data(file_id) if file_content is None: logger.warning(f"No content found for file with ID {file_id}") continue @@ -1047,7 +1054,7 @@ JSON_OUTPUT = {{ data = data.encode('utf-8') # Save file in the database - file_meta = self.lucy_interface.save_uploaded_file(data, name) + file_meta = self.mydom.save_uploaded_file(data, name) if file_meta and "id" in file_meta: # Update the Document with the File-ID document["file_id"] = file_meta["id"] @@ -1123,7 +1130,7 @@ JSON_OUTPUT = {{ workflow_status = workflow.get("status", "running") # Set agent_name from global settings - agent_name = GLOBAL_SETTINGS.get("system_name", "AI Assistant") + agent_name = GLOBAL_WorkflowLabels.get("system_name", "AI Assistant") # Create log entry log_entry = { @@ -1144,7 +1151,7 @@ JSON_OUTPUT = {{ workflow["logs"].append(log_entry) # Save in database - self.lucy_interface.create_workflow_log(log_entry) + self.mydom.create_workflow_log(log_entry) # Also log in logger if level == "info": diff --git a/modules/chat_agent_analyst.py b/modules/chat_agent_analyst.py index 39bf6520..e2c9cb98 100644 --- a/modules/chat_agent_analyst.py +++ b/modules/chat_agent_analyst.py @@ -35,9 +35,9 @@ class AgentAnalyst(AgentBase): # Set default visualization settings plt.style.use('seaborn-v0_8-whitegrid') - def set_dependencies(self, ai_service=None): + def set_dependencies(self, mydom=None): """Set external dependencies for the agent.""" - self.ai_service = ai_service + self.mydom = mydom async def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]: """ @@ -56,7 +56,7 @@ class AgentAnalyst(AgentBase): output_specs = task.get("output_specifications", []) # Check AI service - if not self.ai_service: + if not self.mydom: return { "feedback": "The Analyst agent requires an AI service to function.", "documents": [] @@ -105,9 +105,9 @@ class AgentAnalyst(AgentBase): documents.append(document) # Generate feedback - feedback = f"Analysis complete. Created {len(documents)} documents based on your requirements." + feedback = f"{analysis_plan.get('analysis_approach')}" if analysis_plan.get("key_insights"): - feedback += f"\n\nKey insights: {analysis_plan.get('key_insights')}" + feedback += f"\n\n{analysis_plan.get('key_insights')}" return { "feedback": feedback, @@ -251,12 +251,11 @@ class AgentAnalyst(AgentBase): Only return valid JSON. No preamble or explanations. """ - try: - response = await self.ai_service.call_api([ + response = await self.mydom.call_ai([ {"role": "system", "content": "You are a data analysis expert. Respond with valid JSON only."}, {"role": "user", "content": analysis_prompt} - ]) + ], produce_user_answer = True) # Extract JSON from response json_start = response.find('{') @@ -375,10 +374,10 @@ class AgentAnalyst(AgentBase): try: # Get visualization code from AI - viz_code = await self.ai_service.call_api([ + viz_code = await self.mydom.call_ai([ {"role": "system", "content": "You are a data visualization expert. Provide only executable Python code."}, {"role": "user", "content": viz_prompt} - ]) + ], produce_user_answer = True) # Clean code viz_code = viz_code.replace("```python", "").replace("```", "").strip() @@ -504,10 +503,10 @@ class AgentAnalyst(AgentBase): try: # Get data processing code from AI - data_code = await self.ai_service.call_api([ + data_code = await self.mydom.call_ai([ {"role": "system", "content": "You are a data processing expert. Provide only executable Python code."}, {"role": "user", "content": data_prompt} - ]) + ], produce_user_answer = True) # Clean code data_code = data_code.replace("```python", "").replace("```", "").strip() @@ -630,10 +629,10 @@ class AgentAnalyst(AgentBase): try: # Get document content from AI - document_content = await self.ai_service.call_api([ + document_content = await self.mydom.call_ai([ {"role": "system", "content": f"You are a data analysis expert creating a {format_type} document."}, {"role": "user", "content": analysis_prompt} - ]) + ], produce_user_answer = True) # Clean HTML or Markdown if needed if format_type in ["md", "markdown"] and not document_content.strip().startswith("#"): diff --git a/modules/chat_agent_coder.py b/modules/chat_agent_coder.py index b45b8007..3d4ad304 100644 --- a/modules/chat_agent_coder.py +++ b/modules/chat_agent_coder.py @@ -33,12 +33,13 @@ class AgentCoder(AgentBase): ] # Executor settings - self.executor_timeout = APP_CONFIG.get("Agent_Coder_EXECUTION_TIMEOUT") # seconds + self.executor_timeout = int(APP_CONFIG.get("Agent_Coder_EXECUTION_TIMEOUT")) # seconds + self.execution_retry_limit = int(APP_CONFIG.get("Agent_Coder_EXECUTION_RETRY")) # max retries self.temp_dir = None - def set_dependencies(self, ai_service=None): + def set_dependencies(self, mydom=None): """Set external dependencies for the agent.""" - self.ai_service = ai_service + self.mydom = mydom async def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]: """ @@ -58,7 +59,7 @@ class AgentCoder(AgentBase): output_specs = task.get("output_specifications", []) # Check if AI service is available - if not self.ai_service: + if not self.mydom: logger.error("No AI service configured for the Coder agent") return { "feedback": "The Coder agent is not properly configured.", @@ -121,13 +122,13 @@ class AgentCoder(AgentBase): "documents": quick_completion.get("documents", []) } else: - logger.debug(f"Code to generate, quick check responded: {quick_completion.get("prompt", "(no answer)")}") - + logger.debug(f"Code to generate, quick check responded: {quick_completion.get('prompt', '(no answer)')}") + # If quick completion not possible, continue with code generation and execution logger.info("Generating code to solve the task") # 4. Generate code using AI - code, requirements = await self._generate_code(prompt, document_data) + code, requirements = await self._generate_code(prompt) if not code: return { @@ -136,21 +137,69 @@ class AgentCoder(AgentBase): } # 5. Replace the placeholder with actual input_files data - document_data_json = json.dumps(document_data) - code_with_data = code.replace("input_files = \"=== JSONLOAD ===\"", f"input_files = {document_data_json }") + document_data_json = repr(document_data) + code_with_data = code.replace("input_files = \"=== JSONLOAD ===\"", f"input_files = {document_data_json}") - # 6. Execute code and get results - execution_result = self._execute_code(code_with_data, requirements) + # 6. Execute code with retry logic + retry_count = 0 + max_retries = self.execution_retry_limit + execution_history = [] + + while retry_count <= max_retries: + execution_result = self._execute_code(code_with_data, requirements) + execution_history.append({ + "attempt": retry_count + 1, + "code": code_with_data, + "result": execution_result + }) + + # Check if execution was successful + if execution_result.get("success", False): + logger.info(f"Code execution succeeded on attempt {retry_count + 1}") + break + + # If we've reached max retries, exit the loop + if retry_count >= max_retries: + logger.info(f"Reached maximum retry limit ({max_retries}). Giving up.") + break + + # Log the error and attempt to improve the code + error = execution_result.get("error", "Unknown error") + logger.info(f"Execution attempt {retry_count + 1} failed: {error}. Attempting to improve code.") + + # Generate improved code based on error + improved_code, improved_requirements = await self._improve_code( + original_code=code_with_data, + error=error, + execution_result=execution_result, + attempt=retry_count + 1 + ) + + if improved_code: + code_with_data = improved_code + requirements = improved_requirements + logger.info(f"Code improved for retry {retry_count + 2}") + else: + logger.warning("Failed to improve code, using original code for retry") + + retry_count += 1 # 7. Process results and create output documents documents = [] - # Always add the code document + # Always add the final code document documents.append({ "label": "generated_code.py", "content": code_with_data }) + # Add execution history document + execution_history_str = json.dumps(execution_history, indent=2) + documents.append({ + "label": "execution_history.json", + "content": execution_history_str + }) + # Create documents based on execution results if execution_result.get("success", False): result_data = execution_result.get("result") @@ -179,7 +228,10 @@ class AgentCoder(AgentBase): "content": execution_result.get("output", "") }) - feedback = "Code executed successfully. Generated output files based on specifications." + if retry_count > 0: + feedback = f"Code executed successfully after {retry_count + 1} attempts. Generated output files based on specifications." + else: + feedback = "Code executed successfully. Generated output files based on specifications." else: # Execution failed error = execution_result.get("error", "Unknown error") @@ -187,13 +239,92 @@ class AgentCoder(AgentBase): "label": "execution_error.txt", "content": f"Error executing code:\n\n{error}" }) - feedback = f"Error during code execution: {error}" + + if retry_count > 0: + feedback = f"Error during code execution after {retry_count + 1} attempts: {error}" + else: + feedback = f"Error during code execution: {error}" return { "feedback": feedback, "documents": documents } + async def _improve_code(self, original_code: str, error: str, execution_result: Dict[str, Any], attempt: int) -> Tuple[str, List[str]]: + """ + Improve code based on execution error. + + Args: + original_code: The code that failed to execute + error: The error message + execution_result: Complete execution result dictionary + attempt: Current attempt number + + Returns: + Tuple of (improved_code, requirements) + """ + # Create prompt for code improvement + improvement_prompt = f""" +Fix the following Python code that failed during execution. This is attempt {attempt} to fix the code. + +ORIGINAL CODE: +{original_code} + +ERROR MESSAGE: +{error} + +STDOUT: +{execution_result.get('output', '')} + +INSTRUCTIONS: +1. Fix all errors identified in the error message +2. Diagnose and fix any logical issues +3. Pay special attention to: + - Type conversions and data handling + - Error handling and edge cases + - Resource management (file handles, etc.) + - Syntax errors and typos +4. Keep the input_files handling logic intact +5. Maintain the same overall structure and purpose + +OUTPUT: +- Your improved code MUST still define a 'result' variable as a dictionary +- Each output file should be a key in the result dictionary +- DO NOT remove the input_files assignment line structure + +REQUIREMENTS: +Required packages should be specified as: +# REQUIREMENTS: library==version,library2>=version +- You may add/remove requirements as needed to fix the code + +Return ONLY Python code without explanations or markdown. +""" + + # Call AI service + messages = [ + {"role": "system", "content": "You are an expert Python code debugger. Provide only fixed Python code without explanations or formatting."}, + {"role": "user", "content": improvement_prompt} + ] + + try: + improved_content = await self.mydom.call_ai(messages, temperature=0.2) + + # Extract code and requirements + improved_code = self._clean_code(improved_content) + + # Extract requirements + requirements = [] + for line in improved_code.split('\n'): + if line.strip().startswith("# REQUIREMENTS:"): + req_str = line.replace("# REQUIREMENTS:", "").strip() + requirements = [r.strip() for r in req_str.split(',') if r.strip()] + break + + return improved_code, requirements + except Exception as e: + logger.error(f"Error improving code: {str(e)}") + return None, [] + async def _check_quick_completion(self, prompt: str, content_extraction: List[Dict], output_specs: List[Dict]) -> Dict: """ Check if the task can be completed without writing and executing code. @@ -245,7 +376,7 @@ Only return valid JSON. Your entire response must be parseable as JSON. """ # Call AI service - logger.debug("Checking if task can be completed without code execution") + logger.debug(f"Checking if task can be completed without code execution: {check_prompt}") messages = [ {"role": "system", "content": "You are an AI assistant that determines if tasks require code execution. Reply with JSON only."}, {"role": "user", "content": check_prompt} @@ -253,7 +384,7 @@ Only return valid JSON. Your entire response must be parseable as JSON. try: # Use a lower temperature for more deterministic response - response = await self.ai_service.call_api(messages, temperature=0.1) + response = await self.mydom.call_ai(messages, produce_user_answer = True, temperature=0.1) # Parse response as JSON if response: @@ -279,7 +410,7 @@ Only return valid JSON. Your entire response must be parseable as JSON. # Default to requiring code execution return None - async def _generate_code(self, prompt: str, input_files: List) -> Tuple[str, List[str]]: + async def _generate_code(self, prompt: str) -> Tuple[str, List[str]]: """ Generate Python code from a prompt with the input_files placeholder. @@ -297,20 +428,19 @@ Generate Python code to solve the following task: TASK: {prompt} -IMPORTANT: -- All input files are provided in the 'input_files' variable as a list of [filename, data, is_base64]. -- The 'input_files' variable is already defined at the top of your code, DO NOT modify it. -- For each file, you can access: - - filename: The name of the file (e.g., "image.png") - - data: The content of the file (base64 encoded or plain text) - - is_base64: Boolean flag indicating if the data is base64 encoded +INPUT FILES: +- 'input_files' variable is provided as [[filename, data, is_base64], ...] +- For text files (is_base64=False): use data directly as string +- For binary files (is_base64=True): use base64.b64decode(data) -- To use a file's data: - - For text files (when is_base64=False): Use the data directly as a string - - For binary files (when is_base64=True): Use base64.b64decode(data) to get bytes - -- Do not perform any additional base64 detection - rely on the is_base64 flag +CODE QUALITY: +- Use explicit type conversions where needed (int/float/str) +- Implement feature detection, not version checks +- Handle errors gracefully with appropriate fallbacks +- Follow latest API conventions for libraries +- Validate inputs before processing +OUTPUT: - Your code MUST define a 'result' variable as a dictionary to store outputs. - Each output file should be a key in the result dictionary. - For example: result = {{"output.txt": "output text", "results.json": json_string}} @@ -318,10 +448,13 @@ IMPORTANT: Your code must start with: input_files = "=== JSONLOAD ===" # DO NOT CHANGE THIS LINE +REQUIREMENTS: Required packages should be specified as: -# REQUIREMENTS: package1,package2,package3 +# REQUIREMENTS: library==version,library2>=version +- Specify exact versions for critical libraries +- Use constraint operators (==,>=,<=) as needed -Return ONLY Python code without explanations or markdown formatting. +Return ONLY Python code without explanations or markdown. """ # Call AI service @@ -330,7 +463,7 @@ Return ONLY Python code without explanations or markdown formatting. {"role": "user", "content": ai_prompt} ] - generated_content = await self.ai_service.call_api(messages, temperature=0.1) + generated_content = await self.mydom.call_ai(messages, temperature=0.1) # Extract code and requirements code = self._clean_code(generated_content) @@ -372,20 +505,23 @@ Return ONLY Python code without explanations or markdown formatting. # 2. Install requirements if provided if requirements: - logger.debug(f"Installing requirements: {requirements}") + logger.info(f"Installing requirements: {requirements}") # Create requirements.txt req_file = os.path.join(self.temp_dir, "requirements.txt") with open(req_file, "w") as f: f.write("\n".join(requirements)) + x="\n".join(requirements) + logger.info(f"Requirements file: {x}.") + # Install requirements try: pip_result = subprocess.run( [python_exe, "-m", "pip", "install", "-r", req_file], capture_output=True, text=True, - timeout=APP_CONFIG.get("Agent_Coder_INSTALL_TIMEOUT") + timeout=int(APP_CONFIG.get("Agent_Coder_INSTALL_TIMEOUT")) ) if pip_result.returncode != 0: logger.debug(f"Error installing requirements: {pip_result.stderr}") diff --git a/modules/chat_agent_documentation.py b/modules/chat_agent_documentation.py index e084c977..3ae98b58 100644 --- a/modules/chat_agent_documentation.py +++ b/modules/chat_agent_documentation.py @@ -27,9 +27,9 @@ class AgentDocumentation(AgentBase): "knowledge_organization" ] - def set_dependencies(self, ai_service=None): + def set_dependencies(self, mydom=None): """Set external dependencies for the agent.""" - self.ai_service = ai_service + self.mydom = mydom async def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]: """ @@ -48,7 +48,7 @@ class AgentDocumentation(AgentBase): output_specs = task.get("output_specifications", []) # Check AI service - if not self.ai_service: + if not self.mydom: return { "feedback": "The Documentation agent requires an AI service to function.", "documents": [] @@ -201,7 +201,7 @@ class AgentDocumentation(AgentBase): """ try: - response = await self.ai_service.call_api([ + response = await self.mydom.call_ai([ {"role": "system", "content": "You are a documentation expert. Respond with valid JSON only."}, {"role": "user", "content": analysis_prompt} ]) @@ -371,10 +371,10 @@ class AgentDocumentation(AgentBase): The introduction should be professional and engaging, formatted according to {format_type} standards. """ - introduction = await self.ai_service.call_api([ + introduction = await self.mydom.call_ai([ {"role": "system", "content": f"You are a documentation expert creating an introduction in {format_type} format."}, {"role": "user", "content": intro_prompt} - ]) + ], produce_user_answer = True) # Step 2: Generate executive summary (if applicable) if document_type in ["report", "whitepaper", "case study"]: @@ -397,10 +397,10 @@ class AgentDocumentation(AgentBase): Keep the summary focused and impactful, approximately 200-300 words. """ - executive_summary = await self.ai_service.call_api([ + executive_summary = await self.mydom.call_ai([ {"role": "system", "content": f"You are a documentation expert creating an executive summary in {format_type} format."}, {"role": "user", "content": summary_prompt} - ]) + ], produce_user_answer = True) else: executive_summary = "" @@ -447,10 +447,10 @@ class AgentDocumentation(AgentBase): Be thorough in your coverage of this section, providing substantive content. """ - section_content = await self.ai_service.call_api([ + section_content = await self.mydom.call_ai([ {"role": "system", "content": f"You are a documentation expert creating detailed content for the {section_title} section."}, {"role": "user", "content": section_prompt} - ]) + ], produce_user_answer = True) sections.append(section_content) @@ -474,10 +474,10 @@ class AgentDocumentation(AgentBase): The conclusion should be professional and impactful, formatted according to {format_type} standards. """ - conclusion = await self.ai_service.call_api([ + conclusion = await self.mydom.call_ai([ {"role": "system", "content": f"You are a documentation expert creating a conclusion in {format_type} format."}, {"role": "user", "content": conclusion_prompt} - ]) + ], produce_user_answer = True) # Step 5: Assemble the complete document if format_type in ["md", "markdown"]: diff --git a/modules/chat_agent_webcrawler.py b/modules/chat_agent_webcrawler.py index 6992ea52..f78857cc 100644 --- a/modules/chat_agent_webcrawler.py +++ b/modules/chat_agent_webcrawler.py @@ -43,9 +43,9 @@ class AgentWebcrawler(AgentBase): self.search_engine = APP_CONFIG.get("Agent_Webcrawler_SEARCH_ENGINE", "https://html.duckduckgo.com/html/?q=") self.user_agent = APP_CONFIG.get("Agent_Webcrawler_USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - def set_dependencies(self, ai_service=None): + def set_dependencies(self, mydom=None): """Set external dependencies for the agent.""" - self.ai_service = ai_service + self.mydom = mydom async def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]: """ @@ -63,7 +63,7 @@ class AgentWebcrawler(AgentBase): output_specs = task.get("output_specifications", []) # Check AI service - if not self.ai_service: + if not self.mydom: return { "feedback": "The Webcrawler agent requires an AI service to function effectively.", "documents": [] @@ -134,7 +134,7 @@ class AgentWebcrawler(AgentBase): try: # Get research plan from AI - response = await self.ai_service.call_api([ + response = await self.mydom.call_ai([ {"role": "system", "content": "You are a web research planning expert. Create precise research plans in JSON format only."}, {"role": "user", "content": research_prompt} ]) @@ -295,8 +295,8 @@ class AgentWebcrawler(AgentBase): Only include information actually found in the content. No fabrications or assumptions. """ - if self.ai_service: - summary = await self.ai_service.call_api([ + if self.mydom: + summary = await self.mydom.call_ai([ {"role": "system", "content": "You summarize web content accurately and concisely, focusing only on what is actually in the content."}, {"role": "user", "content": summary_prompt} ]) @@ -449,7 +449,7 @@ class AgentWebcrawler(AgentBase): try: # Generate report with AI - report_content = await self.ai_service.call_api([ + report_content = await self.mydom.call_ai([ {"role": "system", "content": f"You create professional research reports in {template_format} format."}, {"role": "user", "content": report_prompt} ]) diff --git a/modules/chat_registry.py b/modules/chat_registry.py index be7bad95..ed301fe1 100644 --- a/modules/chat_registry.py +++ b/modules/chat_registry.py @@ -24,11 +24,11 @@ class AgentBase: self.name = "base-agent" self.description = "Basic agent functionality" self.capabilities = [] - self.ai_service = None + self.mydom = None - def set_dependencies(self, ai_service=None): + def set_dependencies(self, mydom=None): """Set external dependencies for the agent.""" - self.ai_service = ai_service + self.mydom = mydom def get_agent_info(self) -> Dict[str, Any]: """ @@ -88,7 +88,7 @@ class AgentRegistry: raise RuntimeError("Singleton instance already exists - use get_instance()") self.agents = {} - self.ai_service = None + self.mydom = None self._load_agents() def _load_agents(self): @@ -146,16 +146,16 @@ class AgentRegistry: except Exception as e: logger.error(f"Error loading agent from module {module_name}: {e}") - def set_ai_service(self, ai_service): + def set_mydom(self, mydom): """Set the AI service for all agents.""" - self.ai_service = ai_service + self.mydom = mydom self.update_agent_dependencies() def update_agent_dependencies(self): """Update dependencies for all registered agents.""" for agent_id, agent in self.agents.items(): if hasattr(agent, 'set_dependencies'): - agent.set_dependencies(ai_service=self.ai_service) + agent.set_dependencies(mydom=self.mydom) def register_agent(self, agent): """ @@ -167,7 +167,7 @@ class AgentRegistry: agent_id = getattr(agent, 'name', "unknown_agent") # Initialize agent with dependencies if hasattr(agent, 'set_dependencies'): - agent.set_dependencies(ai_service=self.ai_service) + agent.set_dependencies(mydom=self.mydom) self.agents[agent_id] = agent logger.debug(f"Agent '{agent.name}' registered") @@ -182,8 +182,8 @@ class AgentRegistry: if agent_identifier in self.agents: agent = self.agents[agent_identifier] # Ensure the agent has the AI service - if hasattr(agent, 'set_dependencies') and self.ai_service: - agent.set_dependencies(ai_service=self.ai_service) + if hasattr(agent, 'set_dependencies') and self.mydom: + agent.set_dependencies(mydom=self.mydom) return agent logger.error(f"Agent with identifier '{agent_identifier}' not found") return None diff --git a/modules/lucydom_interface.py b/modules/lucydom_interface.py index aed9ec44..aa983c7f 100644 --- a/modules/lucydom_interface.py +++ b/modules/lucydom_interface.py @@ -1,5 +1,5 @@ """ -Interface to LucyDOM database. +Interface to LucyDOM database and AI Connectors. Uses the JSON connector for data access with added language support. """ @@ -12,9 +12,12 @@ from typing import Dict, Any, List, Optional, Union import importlib import hashlib +# DYNAMIC PART: Connectors to the Interface from connectors.connector_db_json import DatabaseConnector -from modules.configuration import APP_CONFIG +from connectors.connector_aichat_openai import ChatService +# Basic Configurations +from modules.configuration import APP_CONFIG logger = logging.getLogger(__name__) # Custom exceptions for file handling @@ -132,18 +135,12 @@ class LucyDOMInterface: # Language support methods - def set_ai_service(self, ai_service): - """Set the AI service for API calls""" - self.ai_service = ai_service - def set_user_language(self, language_code: str): """Set the user's preferred language""" self.user_language = language_code logger.info(f"User language set to: {language_code}") - - async def call_ai(self, messages: List[Dict[str, str]], - produce_user_answer: bool = False, - temperature: float = None) -> str: + + async def call_ai(self, messages: List[Dict[str, str]], produce_user_answer: bool = False, temperature: float = None) -> str: """ Enhanced AI service call with language support @@ -161,14 +158,15 @@ class LucyDOMInterface: # Add language instruction for user-facing responses if produce_user_answer and self.user_language: + ltext= f"Please respond in '{self.user_language}' language." if messages and messages[0]["role"] == "system": if "language" not in messages[0]["content"].lower(): - messages[0]["content"] = f"Please respond in {self.user_language} language. {messages[0]['content']}" + messages[0]["content"] = f"{ltext} {messages[0]['content']}" else: # Insert a system message with language instruction messages.insert(0, { "role": "system", - "content": f"Please respond in {self.user_language} language." + "content": ltext }) # Call the AI service @@ -636,7 +634,7 @@ class LucyDOMInterface: self.create_file_data(db_file["id"], file_content) # Debug: Export file to static folder - if logger.isEnabledFor(logging.DEBUG): self._export_file_to_static(file_content, db_file["id"], file_name) + self._export_file_to_static(file_content, db_file["id"], file_name) # DEBUG TODO # Debug: Verify database record was created if not db_file: @@ -1174,7 +1172,7 @@ class LucyDOMInterface: "message": log.get("message", ""), "type": log.get("type", "info"), "timestamp": log.get("timestamp", self._get_current_timestamp()), - "agent_name": log.get("agent_name", GLOBAL_SETTINGS.get("system_name", "AI Assistant")), + "agent_name": log.get("agent_name", "(undefined)"), "status": log.get("status", "running"), "progress": log.get("progress", 50) } @@ -1250,17 +1248,6 @@ class LucyDOMInterface: logger.error(f"Error loading workflow state: {str(e)}") return None -# Global settings for the LucyDOM interface -GLOBAL_SETTINGS = { - "system_name": "AI Assistant", # Default system name for logs - "workflow_status_messages": { - "init": "Workflow initialized", - "running": "Running workflow", - "waiting": "Waiting for input", - "completed": "Workflow completed", - "error": "Error in workflow" - } -} # Singleton factory for LucyDOMInterface instances per context _lucydom_interfaces = {} @@ -1272,7 +1259,12 @@ def get_lucydom_interface(mandate_id: int = 0, user_id: int = 0) -> LucyDOMInter """ context_key = f"{mandate_id}_{user_id}" if context_key not in _lucydom_interfaces: - _lucydom_interfaces[context_key] = LucyDOMInterface(mandate_id, user_id) + # Create new interface instance + interface = LucyDOMInterface(mandate_id, user_id) + # Initialize AI service + ai_service = ChatService() + interface.ai_service = ai_service # Directly set the attribute + _lucydom_interfaces[context_key] = interface return _lucydom_interfaces[context_key] # Init diff --git a/modules/lucydom_model.py b/modules/lucydom_model.py index 80324bc8..746ffc56 100644 --- a/modules/lucydom_model.py +++ b/modules/lucydom_model.py @@ -72,6 +72,7 @@ class FileData(BaseModel): id: int = Field(description="Unique ID of the data object") data: str = Field(description="Binary content of the file as base64 string") + # Workflow model classes class DocumentContent(BaseModel): @@ -99,7 +100,7 @@ class DataStats(BaseModel): bytes_sent: Optional[int] = Field(None, description="Bytes sent") bytes_received: Optional[int] = Field(None, description="Bytes received") -class Message(BaseModel): +class WorkflowMessage(BaseModel): """Message object in the workflow""" id: str = Field(description="Unique ID of the message") workflow_id: str = Field(description="Reference to the parent workflow") @@ -116,6 +117,19 @@ class Message(BaseModel): content: Optional[str] = Field(None, description="Text content of the message") agent_name: Optional[str] = Field(None, description="Name of the agent used") +class WorkflowLog(BaseModel): + """Log entry for a workflow""" + id: str = Field(description="Unique ID of the log entry") + workflow_id: str = Field(description="ID of the associated workflow") + message: str = Field(description="Log message content") + type: str = Field(description="Type of log ('info', 'warning', 'error')") + timestamp: str = Field(description="Timestamp of the log entry") + agent_name: str = Field(description="Name of the agent that created the log") + status: str = Field(description="Status of the workflow at log time") + progress: Optional[int] = Field(None, description="Progress value (0-100)") + mandate_id: Optional[int] = Field(None, description="ID of the mandate") + user_id: Optional[int] = Field(None, description="ID of the user") + class Workflow(BaseModel): """Workflow object for multi-agent system""" id: str = Field(description="Unique ID of the workflow") @@ -125,11 +139,13 @@ class Workflow(BaseModel): status: str = Field(description="Status of the workflow ('running', 'completed')") started_at: str = Field(description="Start timestamp") last_activity: str = Field(description="Timestamp of the last activity") + data_stats: Optional[Dict[str, Any]] = Field(None, description="Total statistics") + current_round: int = Field(default=1, description="Current round/iteration of the workflow") message_ids: List[str] = Field(default=[], description="List of message IDs in this workflow") - data_stats: Optional[Dict[str, Any]] = Field(None, description="Total statistics") - messages: List[Message] = Field(default=[], description="Message history") - logs: List[Dict[str, Any]] = Field(default=[], description="Log entries") + messages: List[WorkflowMessage] = Field(default=[], description="Message history (in-memory representation)") + logs: List[WorkflowLog] = Field(default=[], description="Log entries (in-memory representation)") + # Request models for the API diff --git a/notes/changelog.txt b/notes/changelog.txt index 5bd7b8b3..3a3c5a57 100644 --- a/notes/changelog.txt +++ b/notes/changelog.txt @@ -13,6 +13,10 @@ FRONTEND: PRIO1: + +CHECK: If pictures not displayed to check utf-8 encoding in the base64 string!! + +STOP File export to static folder ("TODO) add connector to myoutlook @@ -26,8 +30,6 @@ Split big files into content-parts PRIO2: -implement cleanup routines for files in lucydom_interface (File_Management_CLEANUP_INTERVAL): temp older than interval, all orphaned - Integrate NDA Text as modal form - Data governance agreement by login with checkbox frontend to react diff --git a/static/1_test_document.txt b/static/1_test_document.txt deleted file mode 100644 index 8ddf7560..00000000 --- a/static/1_test_document.txt +++ /dev/null @@ -1,10 +0,0 @@ - - This is a test text file for the ChatManager workflow. - It contains some information for testing document processing. - - The ChatManager should be able to process this file - and extract relevant information from it. - - This file serves as an example for text-based documents that can be - used in a chat workflow. - \ No newline at end of file diff --git a/static/2_test_image.png b/static/2_test_image.png deleted file mode 100644 index 7296313bcd08c0e6fdf6749f999771f959b4c55c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^DIm-Bx4qkSWX+*92ZlJj`MWEQ@p$4a_VYmRGI+ZBxvX - from PIL import Image -ModuleNotFoundError: No module named 'PIL' diff --git a/test2.py b/test2.py deleted file mode 100644 index c25c0e6a..00000000 --- a/test2.py +++ /dev/null @@ -1,50 +0,0 @@ -from PIL import Image - -def calculate_image_pixels(image_path): - try: - with Image.open(image_path) as img: - width, height = img.size - total_pixels = width * height - return total_pixels - except Exception as e: - print(f"Error calculating image pixels: {e}") - return None - -def calculate_text_characters(text_path): - try: - with open(text_path, 'r', encoding='utf-8') as file: - text = file.read() - total_characters = len(text) - return total_characters - except Exception as e: - print(f"Error calculating text characters: {e}") - return None - -def main(): - image_path = 'test_image' - text_path = 'test_document' - - # Calculate total pixels in the image - total_pixels = calculate_image_pixels(image_path) - - # Calculate total characters in the text document - total_characters = calculate_text_characters(text_path) - - # Prepare the result dictionary - result = { - 'total_pixels': total_pixels, - 'total_characters': total_characters - } - - # Write the result to a text file - try: - with open('result.txt', 'w') as result_file: - result_file.write(str(result)) - except Exception as e: - print(f"Error writing result to file: {e}") - - # Output the result - print(result) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_workflow1.py b/test_workflow1.py index 67dc3eb3..43431e8f 100644 --- a/test_workflow1.py +++ b/test_workflow1.py @@ -13,7 +13,7 @@ from datetime import datetime # Configure logging logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', handlers=[logging.StreamHandler()] ) @@ -112,7 +112,7 @@ async def run_chat_workflow(mandate_id: int, user_id: int, file_ids: List[int]) # Create user request user_input = { - "prompt": "Bitte zähle mir zusammen wieviele Pixel das Bild hat und wieviele Zeichen der Text der Dokumente hat", + "prompt": "Bitte integriere den Text der Datei ins angefügte Bild", "list_file_id": file_ids } diff --git a/testcode.py b/testcode.py new file mode 100644 index 00000000..a7c1d246 --- /dev/null +++ b/testcode.py @@ -0,0 +1,66 @@ +input_files = [['test_document.txt', 'CiAgICBUaGlzIGlzIGEgdGVzdCB0ZXh0IGZpbGUgZm9yIHRoZSBDaGF0TWFuYWdlciB3b3JrZmxvdy4KICAgIEl0IGNvbnRhaW5zIHNvbWUgaW5mb3JtYXRpb24gZm9yIHRlc3RpbmcgZG9jdW1lbnQgcHJvY2Vzc2luZy4KICAgIAogICAgVGhlIENoYXRNYW5hZ2VyIHNob3VsZCBiZSBhYmxlIHRvIHByb2Nlc3MgdGhpcyBmaWxlCiAgICBhbmQgZXh0cmFjdCByZWxldmFudCBpbmZvcm1hdGlvbiBmcm9tIGl0LgogICAgCiAgICBUaGlzIGZpbGUgc2VydmVzIGFzIGFuIGV4YW1wbGUgZm9yIHRleHQtYmFzZWQgZG9jdW1lbnRzIHRoYXQgY2FuIGJlCiAgICB1c2VkIGluIGEgY2hhdCB3b3JrZmxvdy4KICAgIA==', True], ['test_document.txt', '\n This is a test text file for the ChatManager workflow.\n It contains some information for testing document processing.\n \n The ChatManager should be able to process this file\n and extract relevant information from it.\n \n This file serves as an example for text-based documents that can be\n used in a chat workflow.\n ', False], ['test_image.png', 'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAA40lEQVR4nO3QsQEAIAyAsOr/P+sLZU9mJs4btu66xKzCrMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArNn7il4Bx2GaB88AAAAASUVORK5CYII=', True], ['test_image.png', 'Image (100x100, PNG, RGB)', False]] # DO NOT CHANGE THIS LINE + +# REQUIREMENTS: Pillow==10.0.0 + +import base64 +from PIL import Image, ImageDraw, ImageFont +import io + +# Initialize result dictionary +result = {} + +# Helper function to decode base64 data +def decode_base64(data: str) -> bytes: + return base64.b64decode(data) + +# Extract input files +text_data = None +image_data = None + +for file_info in input_files: + filename, data, is_base64 = file_info + if filename == 'test_document.txt' and not is_base64: + text_data = data + elif filename == 'test_image.png' and is_base64: + image_data = decode_base64(data) + +# Validate inputs +if text_data is None: + raise ValueError("Text data not found in input files.") +if image_data is None: + raise ValueError("Image data not found in input files.") + +# Load image +image = Image.open(io.BytesIO(image_data)) + +# Prepare to draw on the image +draw = ImageDraw.Draw(image) + +# Use a basic font +try: + font = ImageFont.truetype("arial", 15) +except IOError: + font = ImageFont.load_default() + +# Define text position +text_position = (10, 10) + +# Overlay text onto image +draw.text(text_position, text_data, font=font, fill=(255, 255, 255)) + +# Save the resulting image to a bytes buffer +output_buffer = io.BytesIO() +image.save(output_buffer, format='PNG') +output_buffer.seek(0) + +#result['integrated_image.png'] = base64.b64encode(output_buffer.getvalue()).decode('utf-8') +# binary_data = base64.b64decode(pic_str) +binary_data=output_buffer.getvalue() + +# Write the binary data to a file +with open("./static/test_output.png", "wb") as f: + f.write(binary_data) + +print("OK") +# Output the result dictionary +result \ No newline at end of file