diff --git a/README.md b/README.md index fc0699be..a45ce82c 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,12 @@ With Cline, you can choose between differnet leading AI model providers: - šŸ’» [LM Studio](https://lmstudio.ai/) - šŸ’» Local LLMs with [Ollama](https://ollama.com/) +- **[Kodu](https://kodu.ai)** + +You can use Kodu with OpenAI compatible APIs: + +- 🧠 [OpenAI API](https://openai.com/api/) + ### Privacy first diff --git a/src/codegate/pipeline/cli/cli.py b/src/codegate/pipeline/cli/cli.py index a18d32ad..abfbe74d 100644 --- a/src/codegate/pipeline/cli/cli.py +++ b/src/codegate/pipeline/cli/cli.py @@ -82,7 +82,7 @@ async def process( base_tool = get_tool_name_from_messages(request) codegate_regex = re.compile(r"^codegate(?:\s+(.*))?", re.IGNORECASE) - if base_tool and base_tool == "cline": + if base_tool and base_tool in ["cline", "kodu"]: # Check if there are or tags tag_match = re.search( r"<(task|feedback)>(.*?)", last_user_message_str, re.DOTALL @@ -111,7 +111,7 @@ async def process( if args: context.shortcut_response = True cmd_out = await codegate_cli(args[1:]) - if base_tool and base_tool == "cline": + if base_tool in ["cline", "kodu"]: cmd_out = ( f"{cmd_out}\n" ) diff --git a/src/codegate/pipeline/codegate_context_retriever/codegate.py b/src/codegate/pipeline/codegate_context_retriever/codegate.py index ac33b700..27ebdd41 100644 --- a/src/codegate/pipeline/codegate_context_retriever/codegate.py +++ b/src/codegate/pipeline/codegate_context_retriever/codegate.py @@ -13,7 +13,7 @@ from codegate.pipeline.extract_snippets.extract_snippets import extract_snippets from codegate.storage.storage_engine import StorageEngine from codegate.utils.package_extractor import PackageExtractor -from codegate.utils.utils import generate_vector_string +from codegate.utils.utils import generate_vector_string, get_tool_name_from_messages logger = structlog.get_logger("codegate") @@ -63,7 +63,7 @@ async def process( last_message = self.get_last_user_message_block(request) if not last_message: return PipelineResult(request=request) - user_message, _ = last_message + user_message, last_user_idx = last_message # Create storage engine object storage_engine = StorageEngine() @@ -124,33 +124,34 @@ async def process( context_str = self.generate_context_str(all_bad_packages, context) context.bad_packages_found = True - last_user_idx = self.get_last_user_message_idx(request) - # Make a copy of the request new_request = request.copy() - # Format: "Context: {context_str} \n Query: {last user message content}" - message = new_request["messages"][last_user_idx] - message_str = str(message["content"]) # type: ignore - # Add the context to the last user message - if message_str.strip().startswith(""): - # formatting of cline - match = re.match(r"()(.*?)()(.*)", message_str, re.DOTALL) - if match: - task_start, task_content, task_end, rest_of_message = match.groups() - - # Embed the context into the task block - updated_task_content = ( - f"{task_start}Context: {context_str}\n" - + f"Query: {task_content.strip()}{task_end}" - ) - - # Combine the updated task block with the rest of the message - context_msg = updated_task_content + rest_of_message - else: - context_msg = f"Context: {context_str} \n\n Query: {message_str}" # type: ignore - message["content"] = context_msg - - logger.debug("Final context message", context_message=context_msg) - + # perform replacement in all the messages starting from this index + for i in range(last_user_idx, len(new_request["messages"])): + message = new_request["messages"][i] + message_str = str(message["content"]) # type: ignore + context_msg = message_str + # Add the context to the last user message + base_tool = get_tool_name_from_messages(request) + if base_tool in ["cline", "kodu"]: + match = re.search(r"\s*(.*?)\s*(.*)", message_str, re.DOTALL) + if match: + task_content = match.group(1) # Content within ... + rest_of_message = match.group(2).strip() # Content after , if any + + # Embed the context into the task block + updated_task_content = ( + f"Context: {context_str}" + + f"Query: {task_content.strip()}" + ) + + # Combine updated task content with the rest of the message + context_msg = updated_task_content + rest_of_message + + else: + context_msg = f"Context: {context_str} \n\n Query: {message_str}" # type: ignore + + new_request["messages"][i]["content"] = context_msg + logger.debug("Final context message", context_message=context_msg) return PipelineResult(request=new_request, context=context) diff --git a/src/codegate/pipeline/secrets/secrets.py b/src/codegate/pipeline/secrets/secrets.py index f2f0fca4..42a16c2e 100644 --- a/src/codegate/pipeline/secrets/secrets.py +++ b/src/codegate/pipeline/secrets/secrets.py @@ -450,10 +450,14 @@ async def process_chunk( or input_context.metadata.get("redacted_secrets_count", 0) == 0 ): return [chunk] - - is_cline_client = any( - "Cline" in str(message.trigger_string or "") - for message in input_context.alerts_raised or [] + tool_name = next( + ( + tool.lower() + for tool in ["Cline", "Kodu"] + for message in input_context.alerts_raised or [] + if tool in str(message.trigger_string or "") + ), + "", ) # Check if this is the first chunk (delta role will be present, others will not) @@ -461,7 +465,7 @@ async def process_chunk( redacted_count = input_context.metadata["redacted_secrets_count"] secret_text = "secret" if redacted_count == 1 else "secrets" # Create notification chunk - if is_cline_client: + if tool_name in ["cline", "kodu"]: notification_chunk = self._create_chunk( chunk, f"\nšŸ›”ļø [CodeGate prevented {redacted_count} {secret_text}]" diff --git a/src/codegate/pipeline/system_prompt/codegate.py b/src/codegate/pipeline/system_prompt/codegate.py index 76bcf9d1..383fa194 100644 --- a/src/codegate/pipeline/system_prompt/codegate.py +++ b/src/codegate/pipeline/system_prompt/codegate.py @@ -103,4 +103,41 @@ async def process( # Update the existing system prompt request_system_message["content"] = system_prompt + # check if we are in kodu + if "" in new_request.get("stop", []): + # Collect messages from the assistant matching the criteria + relevant_contents = [ + message["content"] + for message in new_request["messages"] + if message["role"] == "assistant" + and ( + message["content"].startswith("**Warning") + or message["content"].startswith("") + ) + ] + + if relevant_contents: + # Combine the contents into a single message + summarized_content = ( + "" + + "".join(relevant_contents) + + "" + ) + + # Replace the messages with a single summarized message + new_request["messages"] = [ + message + for message in new_request["messages"] + if not ( + message["role"] == "assistant" + and ( + message["content"].startswith("**Warning") + or message["content"].startswith("") + ) + ) + ] + + # Append the summarized message to the messages + new_request["messages"].append({"role": "assistant", "content": summarized_content}) + return PipelineResult(request=new_request, context=context) diff --git a/src/codegate/providers/base.py b/src/codegate/providers/base.py index a525d176..515be531 100644 --- a/src/codegate/providers/base.py +++ b/src/codegate/providers/base.py @@ -17,6 +17,7 @@ from codegate.providers.formatting.input_pipeline import PipelineResponseFormatter from codegate.providers.normalizer.base import ModelInputNormalizer, ModelOutputNormalizer from codegate.providers.normalizer.completion import CompletionNormalizer +from codegate.utils.utils import get_tool_name_from_messages logger = structlog.get_logger("codegate") @@ -233,12 +234,7 @@ async def complete( # Execute the completion and translate the response # This gives us either a single response or a stream of responses # based on the streaming flag - is_cline_client = any( - "Cline" in str(message.get("content", "")) for message in data.get("messages", []) - ) - base_tool = "" - if is_cline_client: - base_tool = "cline" + base_tool = get_tool_name_from_messages(data) model_response = await self._completion_handler.execute_completion( provider_request, diff --git a/src/codegate/utils/utils.py b/src/codegate/utils/utils.py index d0be0edd..51b3f931 100644 --- a/src/codegate/utils/utils.py +++ b/src/codegate/utils/utils.py @@ -45,9 +45,7 @@ def get_tool_name_from_messages(data): Returns: str: The name of the tool found in the messages, or None if no match is found. """ - tools = [ - "Cline", - ] + tools = ["Cline", "Kodu"] for message in data.get("messages", []): message_content = str(message.get("content", "")) for tool in tools: