Skip to content

Commit b4e46cf

Browse files
yroblalukehinds
authored andcommitted
feat: first interation for kodu (#778)
* feat: first interation for kodu * fix secrets in kodu * fix documentation
1 parent e7e0c07 commit b4e46cf

File tree

7 files changed

+86
-44
lines changed

7 files changed

+86
-44
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ With Cline, you can choose between differnet leading AI model providers:
9191
- 💻 [LM Studio](https://lmstudio.ai/)
9292
- 💻 Local LLMs with [Ollama](https://ollama.com/)
9393

94+
- **[Kodu](https://kodu.ai)**
95+
96+
You can use Kodu with OpenAI compatible APIs:
97+
98+
- 🧠 [OpenAI API](https://openai.com/api/)
99+
94100

95101
### Privacy first
96102

src/codegate/pipeline/cli/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async def process(
8282
base_tool = get_tool_name_from_messages(request)
8383
codegate_regex = re.compile(r"^codegate(?:\s+(.*))?", re.IGNORECASE)
8484

85-
if base_tool and base_tool == "cline":
85+
if base_tool and base_tool in ["cline", "kodu"]:
8686
# Check if there are <task> or <feedback> tags
8787
tag_match = re.search(
8888
r"<(task|feedback)>(.*?)</\1>", last_user_message_str, re.DOTALL
@@ -111,7 +111,7 @@ async def process(
111111
if args:
112112
context.shortcut_response = True
113113
cmd_out = await codegate_cli(args[1:])
114-
if base_tool and base_tool == "cline":
114+
if base_tool in ["cline", "kodu"]:
115115
cmd_out = (
116116
f"<attempt_completion><result>{cmd_out}</result></attempt_completion>\n"
117117
)

src/codegate/pipeline/codegate_context_retriever/codegate.py

+29-28
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from codegate.pipeline.extract_snippets.extract_snippets import extract_snippets
1414
from codegate.storage.storage_engine import StorageEngine
1515
from codegate.utils.package_extractor import PackageExtractor
16-
from codegate.utils.utils import generate_vector_string
16+
from codegate.utils.utils import generate_vector_string, get_tool_name_from_messages
1717

1818
logger = structlog.get_logger("codegate")
1919

@@ -63,7 +63,7 @@ async def process(
6363
last_message = self.get_last_user_message_block(request)
6464
if not last_message:
6565
return PipelineResult(request=request)
66-
user_message, _ = last_message
66+
user_message, last_user_idx = last_message
6767

6868
# Create storage engine object
6969
storage_engine = StorageEngine()
@@ -124,33 +124,34 @@ async def process(
124124
context_str = self.generate_context_str(all_bad_packages, context)
125125
context.bad_packages_found = True
126126

127-
last_user_idx = self.get_last_user_message_idx(request)
128-
129127
# Make a copy of the request
130128
new_request = request.copy()
131129

132-
# Format: "Context: {context_str} \n Query: {last user message content}"
133-
message = new_request["messages"][last_user_idx]
134-
message_str = str(message["content"]) # type: ignore
135-
# Add the context to the last user message
136-
if message_str.strip().startswith("<task>"):
137-
# formatting of cline
138-
match = re.match(r"(<task>)(.*?)(</task>)(.*)", message_str, re.DOTALL)
139-
if match:
140-
task_start, task_content, task_end, rest_of_message = match.groups()
141-
142-
# Embed the context into the task block
143-
updated_task_content = (
144-
f"{task_start}Context: {context_str}\n"
145-
+ f"Query: {task_content.strip()}</details>{task_end}"
146-
)
147-
148-
# Combine the updated task block with the rest of the message
149-
context_msg = updated_task_content + rest_of_message
150-
else:
151-
context_msg = f"Context: {context_str} \n\n Query: {message_str}" # type: ignore
152-
message["content"] = context_msg
153-
154-
logger.debug("Final context message", context_message=context_msg)
155-
130+
# perform replacement in all the messages starting from this index
131+
for i in range(last_user_idx, len(new_request["messages"])):
132+
message = new_request["messages"][i]
133+
message_str = str(message["content"]) # type: ignore
134+
context_msg = message_str
135+
# Add the context to the last user message
136+
base_tool = get_tool_name_from_messages(request)
137+
if base_tool in ["cline", "kodu"]:
138+
match = re.search(r"<task>\s*(.*?)\s*</task>(.*)", message_str, re.DOTALL)
139+
if match:
140+
task_content = match.group(1) # Content within <task>...</task>
141+
rest_of_message = match.group(2).strip() # Content after </task>, if any
142+
143+
# Embed the context into the task block
144+
updated_task_content = (
145+
f"<task>Context: {context_str}"
146+
+ f"Query: {task_content.strip()}</task>"
147+
)
148+
149+
# Combine updated task content with the rest of the message
150+
context_msg = updated_task_content + rest_of_message
151+
152+
else:
153+
context_msg = f"Context: {context_str} \n\n Query: {message_str}" # type: ignore
154+
155+
new_request["messages"][i]["content"] = context_msg
156+
logger.debug("Final context message", context_message=context_msg)
156157
return PipelineResult(request=new_request, context=context)

src/codegate/pipeline/secrets/secrets.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -450,18 +450,22 @@ async def process_chunk(
450450
or input_context.metadata.get("redacted_secrets_count", 0) == 0
451451
):
452452
return [chunk]
453-
454-
is_cline_client = any(
455-
"Cline" in str(message.trigger_string or "")
456-
for message in input_context.alerts_raised or []
453+
tool_name = next(
454+
(
455+
tool.lower()
456+
for tool in ["Cline", "Kodu"]
457+
for message in input_context.alerts_raised or []
458+
if tool in str(message.trigger_string or "")
459+
),
460+
"",
457461
)
458462

459463
# Check if this is the first chunk (delta role will be present, others will not)
460464
if len(chunk.choices) > 0 and chunk.choices[0].delta.role:
461465
redacted_count = input_context.metadata["redacted_secrets_count"]
462466
secret_text = "secret" if redacted_count == 1 else "secrets"
463467
# Create notification chunk
464-
if is_cline_client:
468+
if tool_name in ["cline", "kodu"]:
465469
notification_chunk = self._create_chunk(
466470
chunk,
467471
f"<thinking>\n🛡️ [CodeGate prevented {redacted_count} {secret_text}]"

src/codegate/pipeline/system_prompt/codegate.py

+37
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,41 @@ async def process(
103103
# Update the existing system prompt
104104
request_system_message["content"] = system_prompt
105105

106+
# check if we are in kodu
107+
if "</kodu_action>" in new_request.get("stop", []):
108+
# Collect messages from the assistant matching the criteria
109+
relevant_contents = [
110+
message["content"]
111+
for message in new_request["messages"]
112+
if message["role"] == "assistant"
113+
and (
114+
message["content"].startswith("**Warning")
115+
or message["content"].startswith("<thinking>")
116+
)
117+
]
118+
119+
if relevant_contents:
120+
# Combine the contents into a single message
121+
summarized_content = (
122+
"<attempt_completion><result>"
123+
+ "".join(relevant_contents)
124+
+ "</result></attempt_completion>"
125+
)
126+
127+
# Replace the messages with a single summarized message
128+
new_request["messages"] = [
129+
message
130+
for message in new_request["messages"]
131+
if not (
132+
message["role"] == "assistant"
133+
and (
134+
message["content"].startswith("**Warning")
135+
or message["content"].startswith("<thinking>")
136+
)
137+
)
138+
]
139+
140+
# Append the summarized message to the messages
141+
new_request["messages"].append({"role": "assistant", "content": summarized_content})
142+
106143
return PipelineResult(request=new_request, context=context)

src/codegate/providers/base.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from codegate.providers.formatting.input_pipeline import PipelineResponseFormatter
1818
from codegate.providers.normalizer.base import ModelInputNormalizer, ModelOutputNormalizer
1919
from codegate.providers.normalizer.completion import CompletionNormalizer
20+
from codegate.utils.utils import get_tool_name_from_messages
2021

2122
logger = structlog.get_logger("codegate")
2223

@@ -233,12 +234,7 @@ async def complete(
233234
# Execute the completion and translate the response
234235
# This gives us either a single response or a stream of responses
235236
# based on the streaming flag
236-
is_cline_client = any(
237-
"Cline" in str(message.get("content", "")) for message in data.get("messages", [])
238-
)
239-
base_tool = ""
240-
if is_cline_client:
241-
base_tool = "cline"
237+
base_tool = get_tool_name_from_messages(data)
242238

243239
model_response = await self._completion_handler.execute_completion(
244240
provider_request,

src/codegate/utils/utils.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ def get_tool_name_from_messages(data):
4545
Returns:
4646
str: The name of the tool found in the messages, or None if no match is found.
4747
"""
48-
tools = [
49-
"Cline",
50-
]
48+
tools = ["Cline", "Kodu"]
5149
for message in data.get("messages", []):
5250
message_content = str(message.get("content", ""))
5351
for tool in tools:

0 commit comments

Comments
 (0)