Skip to content

feat: first interation for kodu #778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/codegate/pipeline/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task> or <feedback> tags
tag_match = re.search(
r"<(task|feedback)>(.*?)</\1>", last_user_message_str, re.DOTALL
Expand Down Expand Up @@ -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"<attempt_completion><result>{cmd_out}</result></attempt_completion>\n"
)
Expand Down
57 changes: 29 additions & 28 deletions src/codegate/pipeline/codegate_context_retriever/codegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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("<task>"):
# formatting of cline
match = re.match(r"(<task>)(.*?)(</task>)(.*)", 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()}</details>{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"<task>\s*(.*?)\s*</task>(.*)", message_str, re.DOTALL)
if match:
task_content = match.group(1) # Content within <task>...</task>
rest_of_message = match.group(2).strip() # Content after </task>, if any

# Embed the context into the task block
updated_task_content = (
f"<task>Context: {context_str}"
+ f"Query: {task_content.strip()}</task>"
)

# 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)
14 changes: 9 additions & 5 deletions src/codegate/pipeline/secrets/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,18 +450,22 @@ 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)
if len(chunk.choices) > 0 and chunk.choices[0].delta.role:
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"<thinking>\n🛡️ [CodeGate prevented {redacted_count} {secret_text}]"
Expand Down
37 changes: 37 additions & 0 deletions src/codegate/pipeline/system_prompt/codegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "</kodu_action>" 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("<thinking>")
)
]

if relevant_contents:
# Combine the contents into a single message
summarized_content = (
"<attempt_completion><result>"
+ "".join(relevant_contents)
+ "</result></attempt_completion>"
)

# 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("<thinking>")
)
)
]

# Append the summarized message to the messages
new_request["messages"].append({"role": "assistant", "content": summarized_content})

return PipelineResult(request=new_request, context=context)
8 changes: 2 additions & 6 deletions src/codegate/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions src/codegate/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading