diff --git a/.env.template b/.env.template index 4898bdf8..9eaf1a43 100644 --- a/.env.template +++ b/.env.template @@ -6,19 +6,15 @@ WORK_DIR= ANTHROPIC_API_KEY= OPENAI_API_KEY= OPENROUTER_API_KEY= -GOOGLE_API_KEY= OLLAMA_MODEL= LOCAL_MODEL_API_BASE= LOCAL_MODEL_NAME= -# Optional, but highly recommended -## For RAG tool of Researcher -COHERE_API_KEY= - -# Optional ## For Manager agent TODOIST_API_KEY= TODOIST_PROJECT_ID= + +# Optional ## For automatic error check LOG_FILE= ## Frontend Feedback @@ -28,8 +24,8 @@ EDIT_TRANSCRIPTION= ## Show planner intermediate reasoning SHOW_LOGIC_PLAN= -# optional - LLM observability -LANGCHAIN_TRACING_V2=true +# Optional - LLM observability +LANGCHAIN_TRACING_V2= LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" LANGCHAIN_API_KEY= LANGCHAIN_PROJECT= \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/template.md b/.github/PULL_REQUEST_TEMPLATE/template.md new file mode 100644 index 00000000..0fb59ae7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/template.md @@ -0,0 +1,15 @@ +## Pull Request Template + +### Description +Please provide a detailed description of the changes made in this pull request. + +### How to use +If you created new functionality, please describe how it could be enabled and used. + +### Related Issue +If this pull request addresses an existing issue, please reference it here (e.g., "Fixes #123"). + +### Checklist +- [ ] I have tested these changes locally. +- [ ] I used docstrings on the begin of every function I created to describe it. Both humans and AI will have no problem to understand my code. +- [ ] I'm making contribution to the `dev` branch. Direct contributions to `master` are not allowed. Don't worry, they will be merged to `master` on the nearest release. diff --git a/docker-compose.yml b/docker-compose.yml index 9fb66bc0..50f50145 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - TODOIST_API_KEY=${TODOIST_API_KEY} - TODOIST_PROJECT_ID=${TODOIST_PROJECT_ID} - - COHERE_API_KEY=${COHERE_API_KEY} - LOG_FILE=${LOG_FILE:-} volumes: - .:/Clean_Coder @@ -29,7 +28,6 @@ services: - WORK_DIR=/work_dir - OPENAI_API_KEY=${OPENAI_API_KEY} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - COHERE_API_KEY=${COHERE_API_KEY} - LOG_FILE=${LOG_FILE:-} volumes: - .:/Clean_Coder diff --git a/manager.py b/manager.py index afdbe311..1cfea36c 100644 --- a/manager.py +++ b/manager.py @@ -16,11 +16,12 @@ from langgraph.graph import StateGraph from src.tools.tools_project_manager import add_task, modify_task, finish_project_planning, reorder_tasks from src.tools.tools_coder_pipeline import prepare_list_dir_tool, prepare_see_file_tool, ask_human_tool +from src.tools.rag.index_file_descriptions import prompt_index_project_files from src.utilities.manager_utils import actualize_tasks_list_and_progress_description, setup_todoist_project_if_needed, get_manager_messages from src.utilities.langgraph_common_functions import call_model, call_tool, multiple_tools_msg, no_tools_msg, empty_message_msg from src.utilities.start_project_functions import set_up_dot_clean_coder_dir from src.utilities.util_functions import join_paths -from src.utilities.llms import init_llms +from src.utilities.llms import init_llms_medium_intelligence from src.utilities.print_formatters import print_formatted import json import os @@ -34,9 +35,13 @@ class Manager: def __init__(self): load_dotenv(find_dotenv()) self.work_dir = os.getenv("WORK_DIR") + # initial project setup set_up_dot_clean_coder_dir(self.work_dir) + setup_todoist_project_if_needed() + prompt_index_project_files() + self.tools = self.prepare_tools() - self.llms = init_llms(tools=self.tools, run_name="Manager") + self.llms = init_llms_medium_intelligence(tools=self.tools, run_name="Manager") self.manager = self.setup_workflow() self.saved_messages_path = join_paths(self.work_dir, ".clean_coder/manager_messages.json") @@ -113,7 +118,6 @@ def setup_workflow(self): def run(self): print_formatted("😀 Hello! I'm Manager agent. Let's plan your project together!", color="green") - setup_todoist_project_if_needed() messages = get_manager_messages(self.saved_messages_path) inputs = {"messages": messages} diff --git a/non_src/tests/manual_tests/planer_scenario_1.py b/non_src/tests/manual_tests/planer_scenario_1.py index df9322f0..bfe7c81e 100644 --- a/non_src/tests/manual_tests/planer_scenario_1.py +++ b/non_src/tests/manual_tests/planer_scenario_1.py @@ -11,8 +11,8 @@ load_dotenv(find_dotenv()) -folder_with_project_files = repo_directory.joinpath("non_src/tests/manual_tests/projects_files", "debugger_scenario_1_files") -tmp_folder = pathlib.Path(__file__).parent.resolve().joinpath("sandbox_work_dir") +folder_with_project_files = repo_directory.joinpath("non_src/tests/manual_tests/projects_files", "planner_scenario_1_files") +tmp_folder = pathlib.Path(__file__).parent.resolve().joinpath("sandbox_work_dir") setup_work_dir(manual_tests_folder=tmp_folder, test_files_dir=folder_with_project_files) task = "Make form wider, with green background. Improve styling." diff --git a/requirements.txt b/requirements.txt index fb742326..61f23600 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,8 +10,6 @@ langchain-ollama==0.2.0 playwright==1.47.0 libsass==0.23.0 openai==1.61.1 -cohere==5.10.0 -langchain-cohere==0.3.0 chromadb==0.4.21 todoist-api-python==2.1.7 termcolor==2.4.0 @@ -30,4 +28,5 @@ pyright==1.1.390 ruff==0.8.2 httpx==0.27.2 questionary==2.1.0 -pathspec==0.12.1 \ No newline at end of file +pathspec==0.12.1 +numpy==1.26.4 \ No newline at end of file diff --git a/single_task_coder.py b/single_task_coder.py index 6cfc59bd..1bdd57b7 100644 --- a/single_task_coder.py +++ b/single_task_coder.py @@ -17,6 +17,7 @@ from src.utilities.start_project_functions import set_up_dot_clean_coder_dir from src.utilities.util_functions import create_frontend_feedback_story from concurrent.futures import ThreadPoolExecutor +from src.tools.rag.index_file_descriptions import prompt_index_project_files use_frontend_feedback = bool(os.getenv("FRONTEND_URL")) @@ -54,8 +55,9 @@ def run_clean_coder_pipeline(task: str, work_dir: str, doc_harvest: bool = False if __name__ == "__main__": work_dir = os.getenv("WORK_DIR") - set_up_dot_clean_coder_dir(work_dir) - task = user_input("Provide task to be executed. ") if not work_dir: raise Exception("WORK_DIR variable not provided. Please add WORK_DIR to .env file") - run_clean_coder_pipeline(task, work_dir) \ No newline at end of file + set_up_dot_clean_coder_dir(work_dir) + prompt_index_project_files() + task = user_input("Provide task to be executed. ") + run_clean_coder_pipeline(task, work_dir) diff --git a/src/agents/debugger_agent.py b/src/agents/debugger_agent.py index 5adf5741..9dff2782 100644 --- a/src/agents/debugger_agent.py +++ b/src/agents/debugger_agent.py @@ -17,7 +17,7 @@ read_coderrules, convert_images, ) -from src.utilities.llms import init_llms +from src.utilities.llms import init_llms_medium_intelligence from src.utilities.langgraph_common_functions import ( call_model, call_tool, ask_human, after_ask_human_condition, multiple_tools_msg, no_tools_msg, agent_looped_human_help, ) @@ -50,7 +50,7 @@ class Debugger(): def __init__(self, files, work_dir, human_feedback, image_paths, playwright_code=None): self.work_dir = work_dir self.tools = prepare_tools(work_dir) - self.llms = init_llms(self.tools, "Debugger") + self.llms = init_llms_medium_intelligence(self.tools, "Debugger") self.system_message = SystemMessage( content=system_prompt_template.format(project_rules=read_coderrules()) ) diff --git a/src/agents/executor_agent.py b/src/agents/executor_agent.py index 5b40c6e6..d873ba45 100644 --- a/src/agents/executor_agent.py +++ b/src/agents/executor_agent.py @@ -7,7 +7,7 @@ from langgraph.graph import StateGraph, END from dotenv import load_dotenv, find_dotenv from langchain.tools import tool -from src.utilities.llms import init_llms +from src.utilities.llms import init_llms_medium_intelligence from src.utilities.print_formatters import print_formatted, print_error from src.utilities.util_functions import ( check_file_contents, exchange_file_contents, bad_tool_call_looped @@ -43,7 +43,7 @@ class Executor(): def __init__(self, files, work_dir): self.work_dir = work_dir self.tools = prepare_tools(work_dir) - self.llms = init_llms(self.tools, "Executor") + self.llms = init_llms_medium_intelligence(self.tools, "Executor") self.system_message = SystemMessage( content=system_prompt_template ) diff --git a/src/agents/frontend_feedback.py b/src/agents/frontend_feedback.py index 430397ba..f4da34cf 100644 --- a/src/agents/frontend_feedback.py +++ b/src/agents/frontend_feedback.py @@ -1,6 +1,6 @@ import os from langchain_core.messages import HumanMessage -from src.utilities.llms import init_llms +from src.utilities.llms import init_llms_medium_intelligence from src.utilities.start_work_functions import read_frontend_feedback_story import base64 import textwrap @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field -llms = init_llms(run_name="Frontend Feedback") +llms = init_llms_medium_intelligence(run_name="Frontend Feedback") llm = llms[0].with_fallbacks(llms[1:]) diff --git a/src/agents/planner_agent.py b/src/agents/planner_agent.py index 15e916f4..408111ab 100644 --- a/src/agents/planner_agent.py +++ b/src/agents/planner_agent.py @@ -7,7 +7,7 @@ from src.utilities.langgraph_common_functions import after_ask_human_condition from src.utilities.user_input import user_input from src.utilities.graphics import LoadingAnimation -from src.utilities.llms import init_llms_high_intelligence, init_llms_mini, init_llms +from src.utilities.llms import init_llms_high_intelligence, init_llms_mini, init_llms_medium_intelligence import os @@ -15,7 +15,7 @@ llms_planners = init_llms_high_intelligence(run_name="Planner") llm_strong = llms_planners[0].with_fallbacks(llms_planners[1:]) -llms_middle_strength = init_llms(run_name="Plan finalizer") +llms_middle_strength = init_llms_medium_intelligence(run_name="Plan finalizer") llm_middle_strength = llms_middle_strength[0].with_fallbacks(llms_middle_strength[1:]) llms_controller = init_llms_mini(run_name="Plan Files Controller") llm_controller = llms_controller[0].with_fallbacks(llms_controller[1:]) @@ -67,7 +67,7 @@ def call_advanced_planner(state): logic_pseudocode = llm_strong.invoke(logic_planner_messages) print_formatted("\nIntermediate planning done. Finalizing plan...", color="light_magenta") if os.getenv("SHOW_LOGIC_PLAN"): - print(logic_pseudocode.content) + print_formatted(logic_pseudocode.content, color="light_yellow") state["plan_finalizer_messages"].append(HumanMessage(content=f"Logic pseudocode plan to follow:\n\n{logic_pseudocode.content}")) plan_finalizer_messages = state["plan_finalizer_messages"] diff --git a/src/agents/researcher_agent.py b/src/agents/researcher_agent.py index c96ed5df..bdea126a 100644 --- a/src/agents/researcher_agent.py +++ b/src/agents/researcher_agent.py @@ -13,7 +13,7 @@ call_model, call_tool, ask_human, after_ask_human_condition, no_tools_msg ) from src.utilities.print_formatters import print_formatted -from src.utilities.llms import init_llms_mini +from src.utilities.llms import init_llms_medium_intelligence import os @@ -27,7 +27,7 @@ @tool def final_response_researcher( files_to_work_on: Annotated[List[str], "List of existing files to potentially introduce changes"], - reference_files: Annotated[List[str], "List of code files useful as a reference without images"], + reference_files: Annotated[List[str], "List of code files useful as a reference. There are files where similar task been implemented already."], template_images: Annotated[List[str], "List of template images"]): """That tool outputs list of files programmer will need to change and paths to graphical patterns if some. Use that tool only when you 100% sure you found all the files programmer will need to modify. @@ -65,7 +65,7 @@ def __init__(self, work_dir): self.tools = [see_file, list_dir, final_response_researcher] if vdb_available(): self.tools.append(retrieve_files_by_semantic_query) - self.llms = init_llms_mini(self.tools, "Researcher") + self.llms = init_llms_medium_intelligence(self.tools, "Researcher") # workflow definition researcher_workflow = StateGraph(AgentState) diff --git a/src/prompts/binary_ranker.prompt b/src/prompts/binary_ranker.prompt new file mode 100644 index 00000000..b5110a6b --- /dev/null +++ b/src/prompts/binary_ranker.prompt @@ -0,0 +1,8 @@ +You are a binary ranker. Evaluate if document can contain answer for a given question. +Question: """{question}""" +Filename: """{filename}""" +Document: """{document}""" + +If the document is relevant to the question, output only '1'. +If it may be useful for programmer as contains similar code, but no relevant directly, also output only '1'. +If it is not relevant at all, output only '0'. diff --git a/src/prompts/describe_file_chunks.prompt b/src/prompts/describe_file_chunks.prompt new file mode 100644 index 00000000..0ebefc5f --- /dev/null +++ b/src/prompts/describe_file_chunks.prompt @@ -0,0 +1,19 @@ +First, get known with info about project (may be useful, may be not): +''' +{coderrules} +''' + +For the reference, you have code of whole file here: +''' +{file_code} +''' + +Describe provided function/file_chunk in 4 sentences or less, focusing only on important information from integration point of view. +Write what function/file chunk is responsible for. + +Go straight to the thing in description, without starting sentence. + +Here is file chunk to describe: +''' +{chunk_code} +''' \ No newline at end of file diff --git a/src/prompts/planner_system.prompt b/src/prompts/planner_system.prompt index 1d6d4165..33496241 100644 --- a/src/prompts/planner_system.prompt +++ b/src/prompts/planner_system.prompt @@ -20,16 +20,13 @@ For additional context, here's the directory tree: Instructions: -1. Plan the logic: - Outline the logic algorithm before proposing code changes. - -2. Draft a detailed modification plan: +1. Draft a detailed modification plan: - Prioritize readability - Follow the DRY (Don't Repeat Yourself) principle - Use meaningful variable names - Write concise code -3. Format code snippets in your plan properly: +2. Format code snippets in your plan properly: In your code snippets, follow udiff format with filename we working on in the header. For each code modification, use the following structure: ```filename.extension @@ -43,3 +40,4 @@ Instructions: Remember: - If you're unsure how to implement a given task, don't improvise. Simply state that you don't know. Assuming is not allowed - just tell "please provide me with more files" when needed. - When adjusting your plan based on user feedback, always provide a complete version of the plan, referenced to original file contents. Don't reference previous plan. +- Previous plan proposition have not been implemented. Always reference your code changes to code files you have in the context, not to the previous plan proposition. diff --git a/src/prompts/researcher_system.prompt b/src/prompts/researcher_system.prompt index bd5d036b..e53aaaa0 100644 --- a/src/prompts/researcher_system.prompt +++ b/src/prompts/researcher_system.prompt @@ -1,21 +1,26 @@ -As a curious filesystem researcher, examine files thoroughly, prioritizing comprehensive checks. -You checking a lot of different folders looking around for interesting files (hey, you are very curious!) before giving the final answer. -The more folders/files you will check, the more they will pay you. -When you discover significant dependencies from one file to another, ensure to inspect both. -Important: you are can not modify any files! You are reasearching only, but modifications will introduce another guys. Do not execute the task, just prepare ground for it's execution. -Your final selection should include files needed to be modified or needed as reference for a programmer -(for example to see how code in similar file implemented). -Avoid recommending unseen or non-existent files in final response. - -You need to point out all files programmer needed to see to execute the task and only that task. Task is: +As a curious filesystem researcher, thoroughly inspect the files for a task by following these steps: + +1. Break down the task to identify which parts of the application are responsible for executing it. Identify the root of the problem. + +2. Search through various folders to find all necessary files needed to modify for completing the task. Explore numerous folders and files to maximize your understanding. + +3. When you find significant dependencies between files, examine both thoroughly. + +4. Remember, you are only researching. Do not modify any files; modifications will be handled by others. Just prepare the groundwork for task execution. + +5. Also identify files that need to be used as a reference for a programmer. Reference files should include examples where similar tasks have been solved or similar coding tools been used and can serve as code guidance. + +6. Only include files that exist and are necessary for the task. You must not provide information about files you haven’t seen or that don’t exist. + +Lastly, list all files the programmer needs to see to execute the task and only include those relevant to this specific task: + ''' {task} ''' -Here is some additional info about project: +Here's some additional information about the project: ''' {project_rules} ''' -First, provide reasoning about results of your previous action. Think what do you need to find now in order to accomplish the task. -Next, call tool(s). You can use up to 3 tool cals simultaniousely to speed up research. \ No newline at end of file +First, think about what you need to find to accomplish the task based on past actions. Then, use up to 3 tools simultaneously to gather this information. \ No newline at end of file diff --git a/src/tools/rag/code_splitter.py b/src/tools/rag/code_splitter.py index bfa71650..9201e389 100644 --- a/src/tools/rag/code_splitter.py +++ b/src/tools/rag/code_splitter.py @@ -3,12 +3,6 @@ RecursiveCharacterTextSplitter, ) - -python_splitter = RecursiveCharacterTextSplitter.from_language( - language=Language.PYTHON, chunk_size=1000, chunk_overlap=0 -) - - code = """ from langchain_openai.chat_models import ChatOpenAI from langchain_community.chat_models import ChatOllama @@ -310,9 +304,60 @@ def load_system_message(): project_rules=read_coderrules() )) """ +extension_to_language = { + 'cpp': 'cpp', + 'go': 'go', + 'java': 'java', + 'kt': 'kotlin', + 'js': 'js', + 'jsx': 'js', + 'vue': 'js', + 'ts': 'ts', + 'tsx': 'ts', + 'mjs': 'js', + 'cjs': 'js', + 'php': 'php', + 'proto': 'proto', + 'py': 'python', + 'rst': 'rst', + 'rb': 'ruby', + 'rs': 'rust', + 'scala': 'scala', + 'swift': 'swift', + 'md': 'markdown', + 'tex': 'latex', + 'html': 'html', + 'sol': 'sol', + 'cs': 'csharp', + 'cob': 'cobol', + 'c': 'c', + 'lua': 'lua', + 'pl': 'perl', + 'hs': 'haskell', + 'ex': 'elixir', + 'ps1': 'powershell', + 'json': 'json', + 'xml': 'xml', + 'bash': 'powershell', + 'zsh': 'powershell', + 'sh': 'powershell', + 'dockerfile': 'proto', +} + + +def split_code(code: str, extension: str, chunk_size: int = 1000): + """Splits code for smaller elements as functions. That allows to describe functions for semantic retrieval tool.""" + language = extension_to_language.get(extension) + if not language: + return [] + splitter = RecursiveCharacterTextSplitter.from_language( + language=Language(language), chunk_size=chunk_size, chunk_overlap=0 + ) + return splitter.split_text(code) + -splitted = python_splitter.split_text(code) -print(RecursiveCharacterTextSplitter.get_separators_for_language(Language.PYTHON)) -for doc in splitted: - print(doc) - print("###") +if __name__ == "__main__": + splitted = split_code(code, "py") + for doc in splitted: + print(doc) + print("###") diff --git a/src/tools/rag/index_file_descriptions.py b/src/tools/rag/index_file_descriptions.py new file mode 100644 index 00000000..a68c1915 --- /dev/null +++ b/src/tools/rag/index_file_descriptions.py @@ -0,0 +1,214 @@ +import os +from pathlib import Path +from langchain.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser +from dotenv import load_dotenv, find_dotenv +import chromadb +import sys +import questionary +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))) +from src.utilities.util_functions import join_paths, read_coderrules +from src.utilities.start_work_functions import file_folder_ignored +from src.utilities.llms import init_llms_mini +from src.tools.rag.code_splitter import split_code +from src.utilities.print_formatters import print_formatted +from src.tools.rag.retrieval import vdb_available +from src.utilities.manager_utils import QUESTIONARY_STYLE +from tqdm import tqdm + +load_dotenv(find_dotenv()) +work_dir = os.getenv("WORK_DIR") + +GOLDEN = "\033[38;5;220m" +MAGENTA = "\033[95m" +RESET = "\033[0m" + +# Customize tqdm's bar format with golden and magenta colors +bar_format = ( + f"{GOLDEN}{{desc}}: {MAGENTA}{{percentage:3.0f}}%{GOLDEN}|" + f"{{bar}}| {MAGENTA}{{n_fmt}}/{{total_fmt}} files " + f"{GOLDEN}[{{elapsed}}<{{remaining}}, {{rate_fmt}}{{postfix}}]{RESET}" +) + +def is_code_file(file_path): + # List of common code file extensions + code_extensions = { + '.js', '.jsx', '.ts', '.tsx', '.vue', '.py', '.rb', '.php', '.java', '.c', '.cpp', '.cs', '.go', '.swift', + '.kt', '.rs', '.htm','.html', '.css', '.scss', '.sass', '.less', '.prompt', + } + return file_path.suffix.lower() in code_extensions + + +# read file content. place name of file in the top +def get_content(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + content = file_path.name + '\n\n' + content + return content + +def collect_file_pathes(subfolders, work_dir): + """ + Collect and return a list of allowed code files from the given subfolders + under the work_dir according to is_code_file criteria and .coderignore patterns. + """ + allowed_files = [] + for folder in subfolders: + for root, _, files in os.walk(work_dir + folder): + for file in files: + file_path = Path(root) / file + if not is_code_file(file_path): + continue + relative_path_str = file_path.relative_to(work_dir).as_posix() + if file_folder_ignored(relative_path_str): + continue + allowed_files.append(file_path) + return allowed_files + + +def write_file_descriptions(subfolders_with_files=['/']): + all_files = collect_file_pathes(subfolders_with_files, work_dir) + coderrules = read_coderrules() + + prompt = ChatPromptTemplate.from_template( +f"""First, get known with info about project (may be useful, may be not): + +''' +{coderrules} +''' + +Describe the code in 4 sentences or less, focusing only on important information from integration point of view. +Write what file is responsible for. + +Go straight to the thing in description, without starting sentence. + +''' +{{code}} +''' +""" + ) + llms = init_llms_mini(tools=[], run_name='File Describer') + llm = llms[0].with_fallbacks(llms[1:]) + chain = prompt | llm | StrOutputParser() + + description_folder = join_paths(work_dir, '.clean_coder/files_and_folders_descriptions') + Path(description_folder).mkdir(parents=True, exist_ok=True) + batch_size = 8 + pbar = tqdm(total=len(all_files), desc=f"[1/2]Describing files", bar_format=bar_format) + + for i in range(0, len(all_files), batch_size): + files_iteration = all_files[i:i + batch_size] + descriptions = chain.batch([get_content(file_path) for file_path in files_iteration]) + + for file_path, description in zip(files_iteration, descriptions): + file_name = file_path.relative_to(work_dir).as_posix().replace('/', '=') + output_path = join_paths(description_folder, f"{file_name}.txt") + + with open(output_path, 'w', encoding='utf-8') as out_file: + out_file.write(description) + + # Update by actual number of files processed in this batch + pbar.update(len(files_iteration)) + + pbar.close() # Don't forget to close the progress bar when done + + + +def write_file_chunks_descriptions(subfolders_with_files=['/']): + """Writes descriptions of whole file chunks in codebase. Gets list of whole files to describe, divides files + into chunks and describes each chunk separately.""" + all_files = collect_file_pathes(subfolders_with_files, work_dir) + coderrules = read_coderrules() + + grandparent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + with open(f"{grandparent_dir}/prompts/describe_file_chunks.prompt", "r") as f: + chunks_describe_template = f.read() + + prompt = ChatPromptTemplate.from_template(chunks_describe_template) + llms = init_llms_mini(tools=[], run_name='File Describer') + llm = llms[0] + chain = prompt | llm | StrOutputParser() + + description_folder = join_paths(work_dir, '.clean_coder/files_and_folders_descriptions') + Path(description_folder).mkdir(parents=True, exist_ok=True) + + # iterate chunks inside of the file + for file_path in tqdm(all_files, desc=f"[2/2]Describing file chunks", + bar_format=bar_format): + file_content = get_content(file_path) + # get file extenstion + extension = file_path.suffix.lstrip('.') + file_chunks = split_code(file_content, extension) + # do not describe chunk of 1-chunk files + if len(file_chunks) <= 1: + continue + descriptions = chain.batch([{'coderrules': coderrules, 'file_code': file_content, 'chunk_code': chunk} for chunk in file_chunks]) + + for nr, description in enumerate(descriptions): + file_name = f"{file_path.relative_to(work_dir).as_posix().replace('/', '=')}_chunk{nr}" + output_path = join_paths(description_folder, f"{file_name}.txt") + + with open(output_path, 'w', encoding='utf-8') as out_file: + out_file.write(description) + + +def upload_descriptions_to_vdb(): + """Uploads descriptions, created by write_file_chunks_descriptions, into vector database.""" + print_formatted("Uploading file descriptions to vector storage...", color='magenta') + chroma_client = chromadb.PersistentClient(path=join_paths(work_dir, '.clean_coder/chroma_base')) + collection_name = f"clean_coder_{Path(work_dir).name}_file_descriptions" + + collection = chroma_client.get_or_create_collection( + name=collection_name + ) + + # read files and upload to base + description_folder = join_paths(work_dir, '.clean_coder/files_and_folders_descriptions') + for root, _, files in os.walk(description_folder): + for file in files: + file_path = Path(root) / file + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + collection.upsert( + documents=[ + content + ], + ids=[file_path.name.replace('=', '/').removesuffix(".txt")], + ) + + +def prompt_index_project_files(): + """ + Checks if the vector database (VDB) is available. + If not, prompts the user via questionary to index project files for better search. + Then asks if yous sure he want to do indexing. Then triggers write_and_index_descriptions(). + """ + if vdb_available(): + return + answer = questionary.select( + "Do you want to index your project files for improving file search?", + choices=["Proceed", "Skip"], + style=QUESTIONARY_STYLE, + instruction="\nHint: Skip for testing Clean Coder; index for real projects." + ).ask() + if answer == "Proceed": + nr_of_files = len(collect_file_pathes(['/'], work_dir)) + answer = questionary.select( + f"Going to index {nr_of_files} files. Indexing could be time-consuming and costly. Are you ready to go?", + choices=["Index", "Skip"], + style=QUESTIONARY_STYLE, + instruction="\nHint: Ensure you provided all files and directories you don't want to index in {WORK_DIR}/.clean_coder/.coderignore to avoid describing trashy files." + ).ask() + if answer == "Index": + write_and_index_descriptions() + + +def write_and_index_descriptions(): + #provide optionally which subfolders needs to be checked, if you don't want to describe all project folder + write_file_descriptions(subfolders_with_files=['/']) + write_file_chunks_descriptions() + + upload_descriptions_to_vdb() + + +if __name__ == "__main__": + write_and_index_descriptions() \ No newline at end of file diff --git a/src/tools/rag/retrieval.py b/src/tools/rag/retrieval.py index 603ca265..5eef5de8 100644 --- a/src/tools/rag/retrieval.py +++ b/src/tools/rag/retrieval.py @@ -1,57 +1,140 @@ import os -import cohere import chromadb from pathlib import Path from dotenv import load_dotenv, find_dotenv +from src.utilities.llms import init_llms_mini +from langchain.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser load_dotenv(find_dotenv()) work_dir = os.getenv("WORK_DIR") -cohere_key = os.getenv("COHERE_API_KEY") -if cohere_key: - cohere_client = cohere.Client(cohere_key) collection_name = f"clean_coder_{Path(work_dir).name}_file_descriptions" def get_collection(): - if cohere_key: - chroma_client = chromadb.PersistentClient(path=os.getenv('WORK_DIR') + '/.clean_coder/chroma_base') - try: - return chroma_client.get_collection(name=collection_name) - except: - # print("Vector database does not exist. (Optional) create it by running src/tools/rag/write_descriptions.py to improve file research capabilities") - return False - return False + chroma_client = chromadb.PersistentClient(path=os.getenv('WORK_DIR') + '/.clean_coder/chroma_base') + try: + return chroma_client.get_collection(name=collection_name) + except: + # print("Vector database does not exist. (Optional) create it by running src/tools/rag/write_descriptions.py to improve file research capabilities") + return False def vdb_available(): return True if get_collection() else False -def retrieve(question): - # collection should be initialized once, in the class init +def retrieve(question: str) -> str: + """ + Retrieve files descriptions by semantic query. + + Parameters: + question (str): The query to retrieve information for. + + Returns: + str: A formatted response with file descriptions of found files. + """ collection = get_collection() retrieval = collection.query(query_texts=[question], n_results=8) - reranked_docs = cohere_client.rerank( - query=question, - documents=retrieval["documents"][0], - top_n=4, - model="rerank-english-v3.0", - #return_documents=True, - ) - reranked_indexes = [result.index for result in reranked_docs.results] + + # Use BinaryRanker to filter relevant documents + binary_ranker = BinaryRanker() + ranking_results = binary_ranker.rank(question, retrieval) + + # Filter documents that are marked as relevant (score = '1') response = "" - for index in reranked_indexes: - filename = retrieval["ids"][0][index] - description = retrieval["documents"][0][index] - response += f"{filename}:\n\n{description}\n\n" - response += "\n\nRemember to see files before adding to final response!" + for filename, score in ranking_results: + if score == '1': + # Find the corresponding document in the retrieval results + idx = retrieval["ids"][0].index(filename) + description = retrieval["documents"][0][idx] + response += f"{filename}:\n\n{description}\n\n###\n\n" + + # If no relevant documents found, return a message + if not response: + return "No relevant documents found for your query." + response += "\n\nRemember to see files before adding to final response!" return response +# New class added for binary ranking with lazy loading. +class BinaryRanker: + """ + A binary document ranker that uses LLM to determine if a document is relevant. + + This class implements lazy loading of the LLM chain, meaning the chain + is only initialized when the rank method is called. It evaluates whether + each document is relevant to a given question, returning a binary score + (0 or 1) for each document. + """ + def __init__(self): + """ + Initialize the BinaryRanker with lazy loading. + + The LLM chain is not created until the rank method is called. + """ + # Lazy-loaded chain; not initialized until rank() is called. + self.chain = None + + def initialize_chain(self): + """ + Initialize the LLM chain if it hasn't been initialized yet. + + This method loads the prompt template from an external file, initializes the LLM, + and builds the chain used for binary document ranking. + """ + if self.chain is None: + # Load the binary ranker prompt from an external file. + grandparent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + file_path = f"{grandparent_dir}/prompts/binary_ranker.prompt" + with open(file_path, 'r') as file_handle: + template_text = file_handle.read() + prompt = ChatPromptTemplate.from_template(template_text) + # Initialize LLMs with minimal intelligence and set run name to 'BinaryRanker' + llms = init_llms_mini(tools=[], run_name='BinaryRanker') + llm = llms[0].with_fallbacks(llms[1:]) + # Build the chain by combining the prompt template, the LLM instance, and StrOutputParser. + self.chain = prompt | llm | StrOutputParser() + + def rank(self, question: str, retrieval: dict) -> list: + """ + Rank documents based on their relevance to the question. + + Parameters: + question (str): The query to evaluate document relevance against. + retrieval (dict): The retrieval results from a vector database query. + + Returns: + list: A list of tuples containing document IDs and their binary relevance scores ('0' or '1'). + """ + # Ensure the chain is initialized (lazy loading) + self.initialize_chain() + # Extract list of documents and their ids from the retrieval result. + documents_list = retrieval["documents"][0] + filenames_list = retrieval["ids"][0] + # Build input for batch processing: list of dicts containing question, filename, and document. + batch_inputs = [] + for idx, doc in enumerate(documents_list): + batch_inputs.append({ + "question": question, + "filename": filenames_list[idx], + "document": doc + }) + # Use the chain batch function to get binary outputs. + results = self.chain.batch(batch_inputs) + # Pair each document id with its binary ranking result. + ranking = [] + for idx, result in enumerate(results): + ranking.append((filenames_list[idx], result.strip())) + return ranking + + if __name__ == "__main__": - question = "Common styles, used in the main page" + # Example usage of BinaryRanker for testing. + question = "Some tool that can change files" + # Test the retrieve function results = retrieve(question) print("\n\n") print("results: ", results) diff --git a/src/tools/rag/write_descriptions.py b/src/tools/rag/write_descriptions.py deleted file mode 100644 index 4518593e..00000000 --- a/src/tools/rag/write_descriptions.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -from pathlib import Path -from langchain.prompts import ChatPromptTemplate -from langchain_core.output_parsers import StrOutputParser -from dotenv import load_dotenv, find_dotenv -import chromadb -import sys -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))) -from src.utilities.util_functions import join_paths, read_coderrules -from src.utilities.start_work_functions import CoderIgnore, file_folder_ignored -from src.utilities.llms import init_llms_mini - - -load_dotenv(find_dotenv()) -work_dir = os.getenv("WORK_DIR") - - -def is_code_file(file_path): - # List of common code file extensions - code_extensions = { - '.js', '.jsx', '.ts', '.tsx', '.vue', '.py', '.rb', '.php', '.java', '.c', '.cpp', '.cs', '.go', '.swift', - '.kt', '.rs', '.htm','.html', '.css', '.scss', '.sass', '.less', '.prompt', - } - return file_path.suffix.lower() in code_extensions - - -# read file content. place name of file in the top -def get_content(file_path): - with open(file_path, 'r', encoding='utf-8') as file: - content = file.read() - content = file_path.name + '\n' + content - return content - -def collect_file_pathes(subfolders, work_dir): - """ - Collect and return a list of allowed code files from the given subfolders - under the work_dir according to is_code_file criteria and .coderignore patterns. - """ - allowed_files = [] - for folder in subfolders: - for root, _, files in os.walk(work_dir + folder): - for file in files: - file_path = Path(root) / file - if not is_code_file(file_path): - continue - relative_path_str = file_path.relative_to(work_dir).as_posix() - if file_folder_ignored(relative_path_str): - continue - allowed_files.append(file_path) - return allowed_files - - -def write_descriptions(subfolders_with_files=['/']): - all_files = collect_file_pathes(subfolders_with_files, work_dir) - - coderrules = read_coderrules() - - prompt = ChatPromptTemplate.from_template( -f"""First, get known with info about project (may be useful, may be not): - -''' -{coderrules} -''' - -Describe the code in 4 sentences or less, focusing only on important information from integration point of view. -Write what file is responsible for. - -Go traight to the thing in description, without starting sentence. - -''' -{{code}} -''' -""" - ) - llms = init_llms_mini(tools=[], run_name='File Describer') - llm = llms[0] - chain = prompt | llm | StrOutputParser() - - description_folder = join_paths(work_dir, '.clean_coder/files_and_folders_descriptions') - Path(description_folder).mkdir(parents=True, exist_ok=True) - # iterate over all files, take 8 files at once - batch_size = 8 - for i in range(0, len(all_files), batch_size): - files_iteration = all_files[i:i + batch_size] - descriptions = chain.batch([get_content(file_path) for file_path in files_iteration]) - print(descriptions) - - for file_path, description in zip(files_iteration, descriptions): - file_name = file_path.relative_to(work_dir).as_posix().replace('/', '=') - output_path = join_paths(description_folder, f"{file_name}.txt") - - with open(output_path, 'w', encoding='utf-8') as out_file: - out_file.write(description) - - -def upload_descriptions_to_vdb(): - chroma_client = chromadb.PersistentClient(path=join_paths(work_dir, '.clean_coder/chroma_base')) - collection_name = f"clean_coder_{Path(work_dir).name}_file_descriptions" - - collection = chroma_client.get_or_create_collection( - name=collection_name - ) - - # read files and upload to base - description_folder = join_paths(work_dir, '.clean_coder/files_and_folders_descriptions') - for root, _, files in os.walk(description_folder): - for file in files: - file_path = Path(root) / file - with open(file_path, 'r', encoding='utf-8') as file: - content = file.read() - collection.upsert( - documents=[ - content - ], - ids=[file_path.name.replace('=', '/').removesuffix(".txt")], - ) - - -if __name__ == '__main__': - #provide optionally which subfolders needs to be checked, if you don't want to describe all project folder - write_descriptions(subfolders_with_files=['/']) - - upload_descriptions_to_vdb() diff --git a/src/utilities/llms.py b/src/utilities/llms.py index 3885dcc9..9d6dfeda 100644 --- a/src/utilities/llms.py +++ b/src/utilities/llms.py @@ -31,16 +31,15 @@ def llm_open_local_hosted(model): timeout=90, ) -def init_llms(tools=None, run_name="Clean Coder", temp=0): +def init_llms_medium_intelligence(tools=None, run_name="Clean Coder", temp=0): llms = [] if getenv("ANTHROPIC_API_KEY"): - llms.append(ChatAnthropic(model='claude-3-5-sonnet-20241022', temperature=temp, timeout=60, max_tokens=2048)) + llms.append(ChatAnthropic(model='claude-3-5-sonnet-latest', temperature=temp, timeout=60, max_tokens=2048)) if getenv("OPENROUTER_API_KEY"): llms.append(llm_open_router("anthropic/claude-3.5-sonnet")) if getenv("OPENAI_API_KEY"): llms.append(ChatOpenAI(model="gpt-4o", temperature=temp, timeout=60)) - # if os.getenv("GOOGLE_API_KEY"): - # llms.append(ChatGoogleGenerativeAI(model="gemini-2.0-flash-exp", temperature=temp, timeout=60)) + if getenv("OLLAMA_MODEL"): llms.append(ChatOllama(model=os.getenv("OLLAMA_MODEL"))) if getenv("LOCAL_MODEL_API_BASE"): @@ -75,18 +74,15 @@ def init_llms_mini(tools=None, run_name="Clean Coder", temp=0): def init_llms_high_intelligence(tools=None, run_name="Clean Coder", temp=0.2): llms = [] + if os.getenv("ANTHROPIC_API_KEY"): + llms.append(ChatAnthropic(model='claude-3-7-sonnet-latest', temperature=temp, timeout=60, max_tokens=4096)) + if getenv("OPENROUTER_API_KEY"): + llms.append(llm_open_router("anthropic/claude-3.7-sonnet")) if os.getenv("OPENAI_API_KEY"): llms.append(ChatOpenAI(model="o3-mini", temperature=1, timeout=60, reasoning_effort="high")) if os.getenv("OPENAI_API_KEY"): llms.append(ChatOpenAI(model="o1", temperature=1, timeout=60)) - if os.getenv("OPENROUTER_API_KEY"): - llms.append(llm_open_router("openai/gpt-4o")) - if os.getenv("OPENAI_API_KEY"): - llms.append(ChatOpenAI(model="gpt-4o", temperature=temp, timeout=60)) - if os.getenv("ANTHROPIC_API_KEY"): - llms.append(ChatAnthropic(model='claude-3-5-sonnet-20241022', temperature=temp, timeout=60, max_tokens=2048)) - # if os.getenv("GOOGLE_API_KEY"): - # llms.append(ChatGoogleGenerativeAI(model="gemini-2.0-flash-exp", temperature=temp, timeout=60)) + if os.getenv("OLLAMA_MODEL"): llms.append(ChatOllama(model=os.getenv("OLLAMA_MODEL"))) if getenv("LOCAL_MODEL_API_BASE"): diff --git a/src/utilities/manager_utils.py b/src/utilities/manager_utils.py index a2b2e560..8a6372a3 100644 --- a/src/utilities/manager_utils.py +++ b/src/utilities/manager_utils.py @@ -6,7 +6,7 @@ from langchain_community.chat_models import ChatOllama from langchain_anthropic import ChatAnthropic from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage, AIMessage -from src.utilities.llms import init_llms +from src.utilities.llms import init_llms_medium_intelligence from src.utilities.util_functions import join_paths, read_coderrules, list_directory_tree from src.utilities.start_project_functions import create_project_plan_file from langchain_core.output_parsers import StrOutputParser @@ -41,7 +41,7 @@ ('highlighted', 'fg:green bold'), # Highlighted choice ('selected', 'fg:green bold'), # Selected choice ('separator', 'fg:magenta'), # Separator between choices - ('instruction', 'fg:white'), # Additional instructions + ('instruction', 'fg:#FFD700'), # Additional instructions now in golden yellow (hex color) ]) @@ -52,7 +52,7 @@ with open(f"{parent_dir}/prompts/manager_progress.prompt", "r") as f: tasks_progress_template = f.read() -llms = init_llms(run_name="Progress description") +llms = init_llms_medium_intelligence(run_name="Progress description") llm = llms[0].with_fallbacks(llms[1:]) @@ -76,6 +76,7 @@ def fetch_epics(): def fetch_tasks(): + print("pies") return todoist_api.get_tasks(project_id=os.getenv('TODOIST_PROJECT_ID')) diff --git a/src/utilities/print_formatters.py b/src/utilities/print_formatters.py index 618e0998..e9c8825e 100644 --- a/src/utilities/print_formatters.py +++ b/src/utilities/print_formatters.py @@ -10,6 +10,9 @@ def print_formatted_content_planner(content): + """ + Prints output of planner module. Highlights code snippets in diff. + """ parts = content.split('```') outside_texts = parts[::2] code_snippets = parts[1::2] diff --git a/src/utilities/syntax_checker_functions.py b/src/utilities/syntax_checker_functions.py index d6b778fc..bc6122a4 100644 --- a/src/utilities/syntax_checker_functions.py +++ b/src/utilities/syntax_checker_functions.py @@ -216,355 +216,134 @@ def parse_yaml(yaml_string): if __name__ == "__main__": code = """ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import ProfileCard from "./components/ProfileCard"; -import PopupNotification from "./components/PopupNotification"; - -interface ProfileItem { - uuid: string; - full_name: string; - short_bio?: string; - bio?: string; -} +'use client'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; + +// Scale indicator component showing agreement levels from 1-5 +const ScaleIndicator = () => ( +
+
+
+ Highly disagree + Highly agree +
+
+ {Array.from({ length: 5 }, (_, i) => i + 1).map((num) => ( +
+
+
+ {num} +
+
+ ))} +
+
+
+); -export default function Home() { - const [activeTab, setActiveTab] = useState<'Explore' | 'Received' | 'Sent' | 'Matches'>('Explore'); - const [exploreItems, setExploreItems] = useState([]); - const [receivedItems, setReceivedItems] = useState([]); - const [sentItems, setSentItems] = useState([]); - const [matchedItems, setMatchedItems] = useState([]); - const [error, setError] = useState(''); - const [notification, setNotification] = useState<{ message: string, type: 'positive' | 'negative' } | null>(null); - const [loading, setLoading] = useState(false); - const [iconLoading, setIconLoading] = useState(true); - const [skip, setSkip] = useState(0); - const [limit] = useState(10); - const [totalExploreItems, setTotalExploreItems] = useState(0); - const sentinelRef = useRef(null); +function NavHeader() { const router = useRouter(); + return ( +
+
+ +

Survey Results

+
+
+ ); +} - function goToProfile(uuid: string) { - const userRole = localStorage.getItem('role'); - if (userRole === "intern") { - router.push(`/campaign/${uuid}`); - } else { - router.push(`/intern/${uuid}`); - } - } - - async function handleConnect(uuid: string) { - setLoading(true); - try { - const token = localStorage.getItem('token'); - if (!token) throw new Error('Authentication token not found'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/create/${uuid}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to create invitation'); - setNotification({ message: 'Invitation sent successfully', type: 'positive' }); - - // Optimistically update the explore list - setExploreItems((prevItems) => prevItems.filter(item => item.uuid !== uuid)); - } catch (err: any) { - setNotification({ message: err.message, type: 'negative' }); - } finally { - setLoading(false); - setTimeout(() => setNotification(null), 3000); - } - } - - async function handleAccept(invitationId: string) { - setLoading(true); - try { - const token = localStorage.getItem('token'); - if (!token) throw new Error('Authentication token not found'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/accept/${invitationId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to accept invitation'); - setNotification({ message: 'Invitation accepted successfully', type: 'positive' }); - setReceivedItems((prevItems) => prevItems.filter(item => item.invitation_id !== invitationId)); - } catch (err: any) { - setNotification({ message: err.message, type: 'negative' }); - } finally { - setLoading(false); - setTimeout(() => setNotification(null), 3000); - } - } - - async function handleReject(invitationId: string) { - setLoading(true); - try { - const token = localStorage.getItem('token'); - if (!token) throw new Error('Authentication token not found'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/reject/${invitationId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to reject invitation'); - setNotification({ message: 'Invitation rejected successfully', type: 'positive' }); - setReceivedItems((prevItems) => prevItems.filter(item => item.invitation_id !== invitationId)); - } catch (err: any) { - setNotification({ message: err.message, type: 'negative' }); - } finally { - setLoading(false); - setTimeout(() => setNotification(null), 3000); - } - } - - async function handleCancel(invitationId: string) { - setLoading(true); - try { - const token = localStorage.getItem('token'); - if (!token) throw new Error('Authentication token not found'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/cancel/${invitationId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to cancel invitation'); - setNotification({ message: 'Invitation canceled successfully', type: 'positive' }); - setSentItems((prevItems) => prevItems.filter(item => item.invitation_id !== invitationId)); - } catch (err: any) { - setNotification({ message: err.message, type: 'negative' }); - } finally { - setLoading(false); - setTimeout(() => setNotification(null), 3000); - } - } - async function fetchExplore() { - try { - const userRole = localStorage.getItem('role'); - const token = localStorage.getItem('token'); - - if (!token) { - throw new Error('Authentication token not found'); - } +export default function Page({ params }: { params: Promise<{ uuid: string }> }) { + const { uuid } = React.use(params); + const [profile, setProfile] = useState(null); + const [error, setError] = useState(''); - if (!userRole) { - throw new Error('User role not found'); + useEffect(() => { + const fetchProfile = async () => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/profile/${uuid}` + ); + if (!response.ok) { + throw new Error('Failed to fetch profile data'); + } + const data = await response.json(); + setProfile(data); + } catch (err: any) { + console.error('Error details:', err); + setError(err.message || 'An error occurred'); } + }; - const url = `${process.env.NEXT_PUBLIC_API_URL}${ - userRole === "intern" - ? '/fetch-campaigns-for-main-page' - : '/fetch-interns-for-main-page' - }?skip=${skip}&limit=${limit}`; - - console.log('Fetching from URL:', url); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || 'Failed to fetch explore items'); - } + fetchProfile(); + }, [uuid]); - const data = await response.json(); - setExploreItems(prev => [...prev, ...(data.items || [])]); - setTotalExploreItems(data.total || 0); - } catch (err: any) { - console.error('Fetch error:', err); - setError(err.message); - setTimeout(() => setError(''), 3000); - } + if (error) { + return ( +
+ {error} +
+ ); } - async function fetchReceived() { - try { - const token = localStorage.getItem('token'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/received`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to fetch received invitations'); - const data = await response.json(); - setReceivedItems(data.items || []); - } catch (err: any) { - setError(err.message); - setTimeout(() => setError(''), 3000); - } + if (!profile) { + return ( +
+ Loading profile data... +
+ ); } - async function fetchSent() { - try { - const token = localStorage.getItem('token'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/invitations/sent`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to fetch sent invitations'); - const data = await response.json(); - setSentItems(data.items || []); - } catch (err: any) { - setError(err.message); - setTimeout(() => setError(''), 3000); - } - } - async function fetchMatches() { - try { - const token = localStorage.getItem('token'); - if (!token) throw new Error('Authentication token not found'); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/matches`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - if (!response.ok) throw new Error('Failed to fetch matches'); - const data = await response.json(); - setMatchedItems(data.items || []); - } catch (err: any) { - setError(err.message); - setTimeout(() => setError(''), 3000); - } + if (!profile.survey_data) { + return
No survey data available.
; } - - useEffect(() => { - const token = localStorage.getItem('token'); - if (!token) { - setError('Please login first'); - return; - } - // Initial load of the first page - fetchExplore().then(() => setSkip(prev => prev + limit)); - }, []); - - // Infinite scroll: Observe the sentinel at the bottom of the Explore list - useEffect(() => { - const observer = new IntersectionObserver((entries) => { - const [entry] = entries; - // If sentinel is in view and we have more items to fetch - if (entry.isIntersecting && skip < totalExploreItems) { - // Fetch the next batch - fetchExplore().then(() => { - setSkip(prev => prev + limit); - }); - } - }); - - if (sentinelRef.current) { - observer.observe(sentinelRef.current); - } - - // Cleanup - return () => { - if (sentinelRef.current) { - observer.unobserve(sentinelRef.current); - } - }; - }, [skip, totalExploreItems, limit]); - - const handleTabClick = (tab: 'Explore' | 'Received' | 'Sent' | 'Matches') => { - setActiveTab(tab); - if (tab === 'Explore') fetchExplore(); - if (tab === 'Received') fetchReceived(); - if (tab === 'Sent') fetchSent(); - if (tab === 'Matches') fetchMatches(); - }; - - let listToRender: ProfileItem[] = []; - if (activeTab === 'Explore') listToRender = exploreItems; - if (activeTab === 'Received') listToRender = receivedItems; - if (activeTab === 'Sent') listToRender = sentItems; - if (activeTab === 'Matches') listToRender = matchedItems; return ( -
-
- -

Glovn

-
- - - - {error && ( -
- {error} +
+
+ + +
+ + {profile.survey_data.map((category) => ( +
+

+ {category.name} +

+ {category.statements.map((statement: any) => ( +
+ + {statement.value} + +

+ {statement.text} +

+
+ ))}
- )} - -
- {listToRender.length === 0 ? ( -
No items found
- ) : ( - listToRender.map((item) => ( - - )) - )} -
- - {/*
*/} - {notification && ( - setNotification(null)} - /> - )} -
+ ))} +
); } - """ print(parse_tsx(code)) \ No newline at end of file diff --git a/src/utilities/user_input.py b/src/utilities/user_input.py index 573ce3ed..6d371b6c 100644 --- a/src/utilities/user_input.py +++ b/src/utilities/user_input.py @@ -3,13 +3,18 @@ from src.utilities.voice_utils import VoiceRecorder import keyboard import readline +import sys recorder = VoiceRecorder() def user_input(prompt=""): - print_formatted(prompt + "Or use (m)icrophone to tell:", color="cyan", bold=True) + print_formatted(prompt + "Or use (m)icrophone to tell, or press Enter for multiline input:", color="cyan", bold=True) + + if not sys.stdin.isatty(): + return sys.stdin.read().strip() + user_sentence = input() if user_sentence == 'm': if not os.getenv("OPENAI_API_KEY"): @@ -26,7 +31,15 @@ def user_input(prompt=""): else: print_formatted("Install 'sudo apt-get install libportaudio2' (Linux) or 'brew install portaudio' (Mac) to use microphone feature.", color="red") user_sentence = input() - + elif user_sentence == '' or '\n' in user_sentence: + if user_sentence: + return user_sentence + print_formatted("Enter your multiline text (end with Ctrl+D on Unix or Ctrl+Z on Windows):", color="green") + try: + user_sentence = sys.stdin.read().strip() + except KeyboardInterrupt: + return "" + return user_sentence diff --git a/src/utilities/util_functions.py b/src/utilities/util_functions.py index 1db64783..57c648e1 100644 --- a/src/utilities/util_functions.py +++ b/src/utilities/util_functions.py @@ -63,9 +63,9 @@ def watch_file(filename, work_dir, line_numbers=True): except FileNotFoundError: return "File not exists." if line_numbers: - formatted_lines = [f"{i + 1}|{line[:-1]} |{i+1}\n" for i, line in enumerate(lines)] + formatted_lines = [f"{i + 1}|{line.rstrip()} |{i+1}\n" for i, line in enumerate(lines)] else: - formatted_lines = [f"{line[:-1]}\n" for line in lines] + formatted_lines = [f"{line.rstrip()}\n" for line in lines] file_content = "".join(formatted_lines) file_content = filename + ":\n\n" + file_content