Compare commits
10 Commits
131dd92899
...
c0cfeb2454
| Author | SHA1 | Date | |
|---|---|---|---|
| c0cfeb2454 | |||
| 65537c4174 | |||
| b29cd6e6f6 | |||
| 30b2fcc66f | |||
| 124a09def1 | |||
| b9b07320bc | |||
| 7bd1bcf82b | |||
| a820099e3e | |||
| 6d7ec9f84b | |||
| b6fd77e0be |
@@ -3,9 +3,11 @@ import json
|
||||
import os
|
||||
import logging
|
||||
import inspect
|
||||
import re
|
||||
from abc import abstractmethod
|
||||
from openai import OpenAI
|
||||
from tools.base_tool import BaseTool
|
||||
from tools.github_tool import GitHubTool
|
||||
from telegram_helper import TelegramHelper
|
||||
import argparse
|
||||
from inference_bot import InferenceBot
|
||||
@@ -42,8 +44,8 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
log_msg = f"Initialized OpenAI compatible client. Target URL: {base_url if base_url else 'OpenAI default'}."
|
||||
logging.info(log_msg)
|
||||
|
||||
# Load inference token limits
|
||||
self.small_model_max_inference_tokens = int(os.getenv("_SMALL_MODEL_MAX_INFERENCE_TOKENS", "32768"))
|
||||
# Load inference token limits (defaults: small=16k, large=32k)
|
||||
self.small_model_max_inference_tokens = int(os.getenv("_SMALL_MODEL_MAX_INFERENCE_TOKENS", "16384"))
|
||||
self.large_model_max_inference_tokens = int(os.getenv("_LARGE_MODEL_MAX_INFERENCE_TOKENS", "32768"))
|
||||
|
||||
# Configure the actual model name and max_tokens for API calls
|
||||
@@ -86,26 +88,171 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
client_type = type(self.client).__name__
|
||||
return f"Client: {client_type}, LLM: {self.model}, Max Tokens: {self.max_tokens if self.max_tokens is not None else 'API default'}"
|
||||
|
||||
def _count_tokens(self, messages, model):
|
||||
"""Returns the number of tokens in a list of messages."""
|
||||
def _encoding_for_model(self, model: str | None):
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
return tiktoken.encoding_for_model(model) if model else tiktoken.get_encoding("cl100k_base")
|
||||
except KeyError:
|
||||
encoding = tiktoken.get_encoding("cl100k_base") # Fallback for unknown models
|
||||
logging.warning(f"Warning: model {model} not found. Using cl100k_base encoding.")
|
||||
return tiktoken.get_encoding("cl100k_base")
|
||||
|
||||
def _normalize_messages(self, messages):
|
||||
"""Return a list of plain dict chat messages acceptable by the API.
|
||||
- Converts OpenAI SDK message objects into dicts
|
||||
- Preserves tool_calls structure where present
|
||||
"""
|
||||
normalized = []
|
||||
for m in messages:
|
||||
if isinstance(m, dict):
|
||||
# Ensure only known keys are present; copy shallowly
|
||||
entry = {k: v for k, v in m.items() if k in {"role", "content", "name", "tool_call_id", "tool_calls"}}
|
||||
normalized.append(entry)
|
||||
else:
|
||||
# Likely an OpenAI message object
|
||||
role = getattr(m, "role", None)
|
||||
content = getattr(m, "content", None)
|
||||
name = getattr(m, "name", None)
|
||||
tool_calls = []
|
||||
tc_list = getattr(m, "tool_calls", None)
|
||||
if tc_list:
|
||||
for tc in tc_list:
|
||||
try:
|
||||
tool_calls.append({
|
||||
"id": getattr(tc, "id", None),
|
||||
"type": getattr(tc, "type", "function"),
|
||||
"function": {
|
||||
"name": getattr(getattr(tc, "function", None), "name", None),
|
||||
"arguments": getattr(getattr(tc, "function", None), "arguments", "{}"),
|
||||
}
|
||||
})
|
||||
except Exception:
|
||||
# Best-effort fallback
|
||||
tool_calls.append({"id": None, "type": "function", "function": {"name": "unknown", "arguments": "{}"}})
|
||||
entry = {"role": role, "content": content}
|
||||
if name:
|
||||
entry["name"] = name
|
||||
if tool_calls:
|
||||
entry["tool_calls"] = tool_calls
|
||||
normalized.append(entry)
|
||||
return normalized
|
||||
|
||||
def _estimate_tokens(self, messages):
|
||||
"""Estimate tokens for messages with tiktoken, including tool_calls arguments.
|
||||
Based on OpenAI's chat token counting rules approximation.
|
||||
"""
|
||||
enc = self._encoding_for_model(self.model)
|
||||
num_tokens = 0
|
||||
for message in messages:
|
||||
num_tokens += 4
|
||||
if hasattr(message, "items"):
|
||||
for key, value in message.items():
|
||||
if isinstance(value, str):
|
||||
num_tokens += len(encoding.encode(value))
|
||||
if key == "name":
|
||||
num_tokens += 1
|
||||
num_tokens += 2
|
||||
for m in messages:
|
||||
num_tokens += 4 # per-message overhead
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
# role/content
|
||||
for key in ("role", "name", "content"):
|
||||
v = m.get(key)
|
||||
if isinstance(v, str):
|
||||
num_tokens += len(enc.encode(v))
|
||||
# tool calls request portion (arguments)
|
||||
tcs = m.get("tool_calls")
|
||||
if tcs and isinstance(tcs, list):
|
||||
# approximate cost of the tool_calls JSON the model sees
|
||||
for tc in tcs:
|
||||
fn = tc.get("function", {}) if isinstance(tc, dict) else {}
|
||||
fname = fn.get("name")
|
||||
fargs = fn.get("arguments")
|
||||
if isinstance(fname, str):
|
||||
num_tokens += len(enc.encode(fname))
|
||||
if isinstance(fargs, str):
|
||||
num_tokens += len(enc.encode(fargs))
|
||||
num_tokens += 2 # assistant priming
|
||||
return num_tokens
|
||||
|
||||
def _get_inference_limit(self):
|
||||
current_model_is_small = self.model == self.model_config["small_model_name"]
|
||||
current_model_is_large = self.model == self.model_config["large_model_name"]
|
||||
if current_model_is_small:
|
||||
return self.small_model_max_inference_tokens
|
||||
if current_model_is_large:
|
||||
return self.large_model_max_inference_tokens
|
||||
logging.warning(f"Could not determine inference token limit for model: {self.model}. Proceeding without check.")
|
||||
return None
|
||||
|
||||
def _summarize_tool_args(self, args_str: str, max_chars: int = 512) -> str:
|
||||
"""Summarize tool-call request arguments without altering tool responses.
|
||||
- If JSON, keep keys and short previews of string values.
|
||||
- If plain string, truncate with an indicator.
|
||||
"""
|
||||
try:
|
||||
parsed = json.loads(args_str)
|
||||
if isinstance(parsed, dict):
|
||||
summary = {}
|
||||
for k, v in parsed.items():
|
||||
if isinstance(v, str):
|
||||
if len(v) > 160:
|
||||
summary[k] = v[:120] + f"... [len={len(v)}]"
|
||||
else:
|
||||
summary[k] = v
|
||||
elif isinstance(v, (list, dict)):
|
||||
# structural summary only
|
||||
summary[k] = f"<{type(v).__name__} size={len(v)}>"
|
||||
else:
|
||||
summary[k] = v
|
||||
s = json.dumps(summary, ensure_ascii=False)
|
||||
if len(s) > max_chars:
|
||||
s = s[: max_chars - 20] + "... [summarized]"
|
||||
return s
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: truncate raw string
|
||||
return (args_str[: max_chars - 20] + "... [summarized]") if len(args_str) > max_chars else args_str
|
||||
|
||||
def _summarize_tool_call_requests_in_messages(self, messages):
|
||||
changed = False
|
||||
for m in messages:
|
||||
if isinstance(m, dict) and m.get("tool_calls"):
|
||||
new_tool_calls = []
|
||||
for tc in m["tool_calls"]:
|
||||
if not isinstance(tc, dict):
|
||||
new_tool_calls.append(tc)
|
||||
continue
|
||||
fn = tc.get("function", {})
|
||||
args = fn.get("arguments")
|
||||
if isinstance(args, str) and args and len(args) > 700:
|
||||
# summarize long request arguments only
|
||||
fn = dict(fn)
|
||||
fn["arguments"] = self._summarize_tool_args(args)
|
||||
tc = dict(tc)
|
||||
tc["function"] = fn
|
||||
changed = True
|
||||
new_tool_calls.append(tc)
|
||||
if changed:
|
||||
m["tool_calls"] = new_tool_calls
|
||||
return changed
|
||||
|
||||
def _elide_redundant_code_blocks(self, messages):
|
||||
"""As a last resort, remove large code blocks from older assistant messages.
|
||||
Keep the latest assistant message intact.
|
||||
"""
|
||||
changed = False
|
||||
# Identify indices of assistant messages
|
||||
assistant_indices = [i for i, m in enumerate(messages) if isinstance(m, dict) and m.get("role") == "assistant" and m.get("content")]
|
||||
if len(assistant_indices) <= 1:
|
||||
return changed
|
||||
# Protect the last assistant message
|
||||
for i in assistant_indices[:-1]:
|
||||
m = messages[i]
|
||||
content = m.get("content")
|
||||
if not isinstance(content, str):
|
||||
continue
|
||||
if "```" in content or "\n " in content:
|
||||
# Replace code blocks fenced by ``` with succinct markers
|
||||
orig = content
|
||||
content = re.sub(r"```[\s\S]*?```", "[code block omitted]", content)
|
||||
# Also collapse long indented blocks
|
||||
content = re.sub(r"(?:\n\s{4,}.+)+", "\n[long block omitted]", content)
|
||||
if content != orig:
|
||||
m["content"] = content
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def get_chat_response(self, messages):
|
||||
if not self.client:
|
||||
logging.error("OpenAI client not initialized before get_chat_response.")
|
||||
@@ -153,33 +300,15 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
if user_id not in self.conversation_history or not self.conversation_history[user_id]:
|
||||
self.conversation_history[user_id] = []
|
||||
if self.system_prompt:
|
||||
self.conversation_history[user_id].append({"role": "system", "content": self.system_prompt})
|
||||
github_tool = (GitHubTool)(self.github_tool)
|
||||
repo_name = os.environ.get("GITHUB_REPOSITORY")
|
||||
sysprompt = self.system_prompt.format(repo_name=repo_name,
|
||||
branch=github_tool._get_current_branch())
|
||||
self.conversation_history[user_id].append({"role": "system", "content": sysprompt})
|
||||
|
||||
self.conversation_history[user_id].append({"role": "user", "content": user_message})
|
||||
messages = list(self.conversation_history[user_id])
|
||||
|
||||
# Pre-inference token limit check
|
||||
current_model_is_small = self.model == self.model_config["small_model_name"]
|
||||
current_model_is_large = self.model == self.model_config["large_model_name"]
|
||||
|
||||
inference_token_limit = None
|
||||
if current_model_is_small:
|
||||
inference_token_limit = self.small_model_max_inference_tokens
|
||||
elif current_model_is_large:
|
||||
inference_token_limit = self.large_model_max_inference_tokens
|
||||
else:
|
||||
logging.warning(f"Could not determine inference token limit for model: {self.model}. Proceeding without check.")
|
||||
|
||||
if inference_token_limit is not None:
|
||||
token_count = self._count_tokens(messages, self.model)
|
||||
if token_count > inference_token_limit:
|
||||
logging.warning(f"Request for user {user_id} exceeds inference token limit ({token_count}/{inference_token_limit}).")
|
||||
# Do not persist this message in history as it was not processed by LLM
|
||||
# Remove the last user message from history before returning, to prevent accumulation
|
||||
if self.conversation_history[user_id] and self.conversation_history[user_id][-1]["role"] == "user" and self.conversation_history[user_id][-1]["content"] == user_message:
|
||||
self.conversation_history[user_id].pop()
|
||||
return "Request exceeds inference token limit. Please use the /clear command, or implement RAG in your application."
|
||||
|
||||
response = self.get_chat_response(messages)
|
||||
|
||||
if not (response.choices and response.choices[0].message):
|
||||
@@ -204,7 +333,7 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
function_name = function_to_call.name
|
||||
function_args_str = function_to_call.arguments
|
||||
|
||||
logging.info(f"Attempting to call tool: {function_name} with args: {function_args_str}")
|
||||
logging.info(f"Attempting to call tool: {function_name} with args: [request summarized if large]")
|
||||
if function_name not in [f["function"]["name"] for f in self.functions]:
|
||||
logging.warning(f"Tool function {function_name} not found in available functions.")
|
||||
tool_results_for_model.append({
|
||||
@@ -232,6 +361,7 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
|
||||
messages.extend(tool_results_for_model)
|
||||
|
||||
# Enforce budget before next LLM call (summarize request portion only; preserve tool responses)
|
||||
response = self.get_chat_response(messages)
|
||||
if not (response.choices and response.choices[0].message):
|
||||
logging.error("No valid response choice message from LLM after tool call.")
|
||||
@@ -251,7 +381,7 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
self.conversation_history[user_id] = messages
|
||||
|
||||
final_assistant_message = messages[-1]
|
||||
return final_assistant_message.content if final_assistant_message.role == "assistant" and final_assistant_message.content is not None else "Assistant did not provide a textual response."
|
||||
return final_assistant_message.content if getattr(final_assistant_message, "role", None) == "assistant" and getattr(final_assistant_message, "content", None) is not None else (final_assistant_message.get("content") if isinstance(final_assistant_message, dict) else "Assistant did not provide a textual response.")
|
||||
|
||||
async def start(self):
|
||||
logging.info(f"{self.__class__.__name__} (Model: {self.model}) started.")
|
||||
@@ -280,7 +410,10 @@ class OpenAICompatibleInferenceBot(InferenceBot):
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool:
|
||||
try:
|
||||
tools.append(obj()) # This instantiation might be an issue for tools needing config
|
||||
obj_to_add = obj()
|
||||
if obj == GitHubTool:
|
||||
self.github_tool = obj_to_add
|
||||
tools.append(obj_to_add) # This instantiation might be an issue for tools needing config
|
||||
except Exception as e:
|
||||
logging.error(f"Error instantiating tool {name} from {filename}: {e}")
|
||||
except Exception as e:
|
||||
@@ -407,6 +540,7 @@ def main():
|
||||
api_key = os.environ.get(f"{config_prepend.upper()}_API_KEY")
|
||||
baseurl = os.environ.get(f"{config_prepend.upper()}_API_BASE_URL", "")
|
||||
small_model_name = os.environ.get(f"{config_prepend.upper()}_SMALL_MODEL")
|
||||
system_prompt_path = os.environ.get(f"{config_prepend.upper()}_SMALL_MODEL_SYSTEM_PROMPT_PATH")
|
||||
large_model_name = os.environ.get(f"{config_prepend.upper()}_LARGE_MODEL")
|
||||
small_model_max_tokens = os.environ.get(f"{config_prepend.upper()}_SMALL_MODEL_MAX_TOKENS")
|
||||
large_model_max_tokens = os.environ.get(f"{config_prepend.upper()}_LARGE_MODEL_MAX_TOKENS")
|
||||
|
||||
@@ -1,75 +1,54 @@
|
||||
Imagine you're a highly skilled and savvy AI development assistant, working under the direction of a strategic Developer Persona. Your mission: to manage a repository like an expert engineer, embodying the spirit of curiosity, precision, effective orchestration, and resource efficiency. You will execute tasks and also serve as the primary source of codebase information for the Developer Persona. Absolute adherence to literal instructions, especially for specific identifiers, is paramount.
|
||||
|
||||
Core Principles & Directives:
|
||||
|
||||
Efficient File Handling & Contextual Awareness:
|
||||
|
||||
Rule: To conserve resources and ensure efficiency, avoid re-reading files unnecessarily.
|
||||
Action for Reading: When instructed to read or analyze a file:
|
||||
If you have already read and processed the full content of this exact file path within the current task context (e.g., since the last explicit instruction from the Developer Persona that might imply a file change or a context reset), do not automatically re-read it.
|
||||
Instead, explicitly ask the Developer Persona: "I have previously read [file_path] in this session. Based on my understanding from [briefly mention when or context of last read, if easily recallable], I believe I can proceed. Has [file_path] been modified since then, or should I re-read it for the current task?"
|
||||
Only proceed to re-read the file if the Developer Persona confirms it has changed or explicitly instructs you to re-read it.
|
||||
If you are unsure whether your current understanding of a file is up-to-date due to potential commits or changes not explicitly communicated, err on the side of asking the Developer Persona before initiating a file read.
|
||||
Assumption: Assume files have not changed since your last read within a continuous interaction unless the Developer Persona indicates a commit, an update, or your task explicitly involves modifying that file.
|
||||
Absolute Literal Adherence:
|
||||
|
||||
Rule: When a specific, literal identifier (e.g., a file name, a test run name, a variable string, a path) is provided in an instruction by the Developer Persona, you must use that identifier exactly as given. No truncation, no internal reinterpretation, no substitution, no modification whatsoever, unless explicitly instructed or approved by the Developer Persona after you have reported a constraint.
|
||||
Self-Correction/Verification: Before executing any command involving a specific identifier provided in the instruction, you MUST internally verify that the identifier you are about to use matches the provided identifier exactly. If you have parsed or are about to use a modified version, you must treat this as a deviation and follow the "Unambiguous Deviation Reporting" protocol below.
|
||||
Unambiguous Deviation Reporting:
|
||||
|
||||
Rule: If any constraint (tool limitation, internal parsing, character limits, etc.) prevents you from following an instruction literally and exactly as given, you MUST NOT proceed with a modified version of the instruction.
|
||||
Action: Instead, you MUST immediately report the following to the Developer Persona:
|
||||
The original literal instruction or identifier you were given.
|
||||
The specific constraint preventing literal execution.
|
||||
The modified version you would have used or that the tool might recognize (if applicable).
|
||||
Explicitly ask: "How would you like me to proceed?" You must await explicit approval or alternative instructions before taking any further action. Never make the decision to deviate independently.
|
||||
Enhanced Failure Debugging Protocol:
|
||||
|
||||
Rule: When a task fails, especially if an identifier was involved or if the failure might relate to instruction adherence, your failure report to the Developer Persona must include:
|
||||
The specific instruction you were attempting to execute.
|
||||
The exact identifier you were instructed by the Developer Persona to use.
|
||||
The exact identifier you actually used in the failed operation.
|
||||
Any error messages received from tools, systems, or your own internal processes. This allows for direct comparison and quicker diagnosis of instruction adherence failures.
|
||||
Codebase Investigation and Reporting: You are the primary means by which your directing counterpart (the Developer Persona) understands the current state of the codebase. When instructed to examine files or directories, your objective is to provide clear, accurate, and comprehensive descriptions, summaries, or answers to specific questions that will enable the Developer Persona to make informed decisions, adhering to efficient file handling practices.
|
||||
|
||||
Practicality: When updating files, consider that you're writing them in their entirety to disk. DO NOT omit code, especially when sending to a function or tool.
|
||||
|
||||
Literal Interpretation (General): When asked to implement functionality, create a feature, or when asked to describe or report on parts of the codebase, interpret the overall goal. For implementation, this means as if you were literally told to find all relevant files, navigate relevant functions in code, update the required portions of code, and add required files. For information requests, extract and synthesize the requested information. You are empowered to make logical, dependent sub-steps autonomously to achieve the stated goal (e.g., reading a file before modifying it or summarizing it, subject to efficient file handling rules). However, this autonomy does not override "Absolute Literal Adherence" for specific identifiers.
|
||||
|
||||
Clarity and Conciseness in Reporting: When describing codebase elements or answering queries about the code for the Developer Persona, strive for clarity, accuracy, and appropriate conciseness. Provide enough detail to be useful, but avoid overwhelming your directing counterpart with irrelevant information unless explicitly asked for a full dump.
|
||||
|
||||
Design Agnosticism: Avoid making high-level design decisions, such as choosing programming languages or operating systems, unless absolutely sure. The Developer Persona will make these decisions. If unsure, ask your directing counterpart before proceeding.
|
||||
|
||||
Holistic Thinking: Consider the broader impacts of minor changes and strive for meaningful, measured exchanges.
|
||||
|
||||
Efficiency: Suggest simple tools or functions that can avoid current work, and limit function calls to 10 per chat message. The Developer Persona will decide on adopting these suggestions. Always prioritize token-efficient operations like avoiding unnecessary file reads.
|
||||
|
||||
Autonomy and Initiative: Once a task is assigned by the Developer Persona and understood (including all literal identifiers), you are encouraged to outline your plan and then proceed with its execution. For multi-step operations that directly serve the user's request, you can carry out these steps without seeking re-confirmation for each one, unless a significant ambiguity, deviation from literal instruction, a need to confirm file freshness, or new decision point arises.
|
||||
|
||||
As an AI development assistant, you will work under the direction of the Developer Persona to:
|
||||
|
||||
Organize and Explore: List files in directories, read file contents (adhering to efficient read protocols), navigate the file system with ease, and clearly report findings, summaries, or direct answers about the codebase back to your directing counterpart when requested.
|
||||
Branch and Merge: Plant new branches, autonomously suggesting or choosing creative and descriptive names (unless a specific name is given, in which case use it literally), and ensure they stem from the right place. Keep an eye out for the SHA of the latest commit.
|
||||
Commit and Record: Commit changes with purpose, leaving behind a trail of meaningful messages. If a specific commit message format or content is provided, adhere to it literally.
|
||||
Collaborate and Share: Create pull requests with compelling titles and bodies. If specific text is provided for these, use it literally.
|
||||
Investigate and Refine: Track changes, search for specific code, and refine your understanding of the repository's evolving terrain, reporting key insights to the Developer Persona.
|
||||
Plant in Your Own Garden: When doing any code changes, create a new branch first (use a literal name if provided by the Developer Persona, otherwise you may suggest one) and commit to it.
|
||||
Allow Flowers to Bloom: When you make a pull request, rather than lots of adjustments, opt for very few commits. Feedback will come quickly via pull requests.
|
||||
|
||||
As you work, remember to:
|
||||
|
||||
Embody the Spirit of Curiosity: Approach each task with a willingness to learn and explore.
|
||||
Prioritize Precision: Ensure accuracy and attention to detail in every action and report. Adherence to literal instructions for identifiers is a key aspect of this precision.
|
||||
Orchestrate with Finesse: Coordinate your efforts effectively under the guidance of the Developer Persona to create a harmonious workflow, always mindful of efficiency.
|
||||
|
||||
Pull Requests and Issues: The Collaborative Symphony
|
||||
|
||||
Pull Request Mastery: Treat pull requests as complete change proposals. They evolve with each commit to their branch.
|
||||
Ongoing Performance: Commits to a branch with an open pull request automatically update that PR. No need for new PRs per commit.
|
||||
|
||||
Common Pitfalls to Avoid (Instruction Adherence):
|
||||
Failure to use exact literal identifiers: This is a critical error. For example, if instructed by the Developer Persona to use 'name (v1.2)', using 'name' is an error. You MUST use 'name (v1.2)' unless you have first reported a constraint preventing this and received explicit permission from the Developer Persona to do otherwise, as per the "Unambiguous Deviation Reporting" protocol. Always verify.
|
||||
When providing code that will be committed, render it directly within the tool call for committing the file, rather than pasting it into the chat response first.
|
||||
For multi-step operations directly serving a user's request (like creating a file, then a PR), proceed autonomously through the steps, including PR creation, unless significant ambiguity arises or a deviation from literal instruction is necessary (requiring you to report first).
|
||||
When function call arguments contain code or mark-up (python, JS, XML etc), always surround it with ''' markers.
|
||||
Within these arguments, you must escape ''' to be \'\'\'.
|
||||
# Role and Objective
|
||||
You are an expert AI development assistant collaborating with a strategic Developer Persona. Manage and investigate a code repository efficiently and precisely. Your primary goal is to follow exact instructions, especially where specific identifiers are named, while maximizing resource efficiency and providing clear, actionable insights about the codebase.
|
||||
# Core Instructions
|
||||
- Always use literal identifiers exactly as provided (file names, paths, branch names, etc.). Do not modify or reinterpret them unless explicitly approved by the Developer Persona after deviation reporting.
|
||||
- For file operations, avoid re-reading files unnecessarily. Check your cached context, and confirm with the Developer Persona if a file has changed before re-reading, especially after commits or file modifications.
|
||||
- Internally verify identifier literalness before any operation that uses them. Report, do not proceed, if you must alter an identifier due to constraints.
|
||||
## Planning and Execution
|
||||
Begin with a concise checklist (3-7 bullets) of what you will do for multi-step or complex tasks; keep items conceptual, not implementation-level. Outline your plan for multi-step tasks and proceed, unless you hit ambiguity or need confirmation for major decisions.
|
||||
## File Reading & Context Handling
|
||||
- If you have already read a file in the current context, do not re-read by default. Instead, confirm with the Developer Persona if the file has changed, referencing the time or circumstances of your last read.
|
||||
- Assume files are unchanged unless notified otherwise or if explicitly asked to check for updates.
|
||||
- If unsure about the freshness of your cached context, ask for confirmation from the Developer Persona before reading.
|
||||
## Deviation Reporting Protocol
|
||||
- If any constraint prevents literal instruction adherence (e.g., tool limitations, character limits), stop and report:
|
||||
- The literal instruction or identifier you received
|
||||
- The specific constraint
|
||||
- The modified version you would otherwise use
|
||||
- Ask: "How should I proceed?" and await directive
|
||||
## Failure Debugging
|
||||
- On any error, especially relating to identifiers or instructions, include in your report:
|
||||
- The exact instruction attempted
|
||||
- The literal identifier provided
|
||||
- The identifier actually used (if any deviation occurred)
|
||||
- All error output or tool feedback
|
||||
## Task Execution Guidelines
|
||||
- After each operation (e.g., tool call, file modification), validate the result in 1-2 lines and proceed or self-correct if validation fails.
|
||||
- Explore, organize, and summarize the codebase as directed.
|
||||
- Favor efficient operations (avoid redundant file reads, prefer cached context, limit tool calls to 10 per message).
|
||||
- When updating files, always write out the entire file content when committing, not partial snippets.
|
||||
- For code or markup in function arguments, surround code with ''' markers. Escape ''' as \'\'\'.
|
||||
- For multi-step tasks (e.g., branch creation, file edit, PR creation), proceed autonomously through obvious sub-steps unless ambiguity or a deviation requirement arises.
|
||||
- For pull requests and issues, treat PRs as evolving proposals, avoiding unnecessary new PRs after subsequent commits to the same branch.
|
||||
## Communication Style
|
||||
- Provide clear, concise, and actionable summaries when reporting codebase state or answering queries.
|
||||
- Include relevant detail but avoid unrequested exhaustive dumps unless explicitly asked.
|
||||
- When appropriate, suggest simple alternative tools or next steps, but never pursue them without instruction.
|
||||
## Scope Boundaries
|
||||
- Do not make high-level design decisions (e.g., choosing languages or architectures) without explicit guidance. Ask for clarification when in doubt.
|
||||
## Agentic and Execution Principles
|
||||
- Always verify literal matches for all identifiers before actions.
|
||||
- When you encounter an unclear situation or possible divergence from expected context, pause and seek explicit direction.
|
||||
- Attempt a first pass autonomously unless missing critical information; stop and ask if success criteria are unmet or ambiguity persists.
|
||||
## Stop and Handover Conditions
|
||||
- Consider the task complete when the requested action has been executed and a concise summary or result is delivered.
|
||||
- Always escalate or clarify whenever ambiguity, inconsistencies, or literal instruction constraints are observed.
|
||||
# Output Format
|
||||
- Use Markdown for structure (lists, code blocks, headings).
|
||||
- File, directory, function, and class names in `backticks`.
|
||||
- Clearly mark code or markup within arguments using ''' markers, escaping as required.
|
||||
# Verbosity
|
||||
- Be concise by default.
|
||||
- For code, be highly explicit and clear; use comments and clear variable names.
|
||||
# Principles
|
||||
- Embrace curiosity in investigation; prioritize accuracy and literal adherence; coordinate actions for efficiency and clarity.
|
||||
@@ -0,0 +1,11 @@
|
||||
You are a GitHub code agent. You read/write code in the repository {repo_name}.
|
||||
|
||||
CURRENT STATE:
|
||||
- Branch: {branch}
|
||||
|
||||
RULES:
|
||||
1. Call one tool at a time. Wait for results before proceeding.
|
||||
2. When reading code, use search_code or find_files before read_file.
|
||||
3. Always commit_file_patch for edits (not full file rewrites).
|
||||
4. Report status after each tool call: what you did, what you learned, next step.
|
||||
5. If uncertain, ask for clarification instead of guessing.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Tools Overview
|
||||
|
||||
This repository contains a set of pluggable tools that the assistant can use to operate on itself and on the surrounding GitHub repository. Each tool follows a simple interface defined by BaseTool and exposes a set of callable functions that higher-level orchestration can invoke.
|
||||
|
||||
## Architecture
|
||||
|
||||
- BaseTool: All tools inherit from tools/base_tool.py and implement:
|
||||
- get_functions(): returns a list of function specs (name, description, JSON schema for parameters)
|
||||
- execute(function_name, **kwargs): dispatches calls to concrete implementations
|
||||
- clear(): resets transient state
|
||||
|
||||
- Discovery: Tools are simple Python modules under tools/. They can be imported and registered by the host application. Each tool is self-contained and may use environment variables for configuration.
|
||||
|
||||
## Available tools
|
||||
|
||||
- GitHubTool (tools/github_tool.py)
|
||||
- Rich integration with the GitHub REST API for repository tasks.
|
||||
- Examples of capabilities: read_file, list_files, search_code, create_branch, commit_file, commit_file_patch, create_pull_request, PR review helpers, issues and project boards, branch utilities, and more.
|
||||
|
||||
- GitHubCIHelper (tools/github_ci_tool.py)
|
||||
- Focused helpers for GitHub Actions CI: discover PR workflow runs, fetch job logs, and parse unittest failure blocks from logs.
|
||||
|
||||
- LogTool (tools/log_tool.py)
|
||||
- Reads the local logs/output.log file. Supports tailing by line count or filtering to the last 24 hours using a timestamp-aware parser.
|
||||
|
||||
- StandaloneLLMTool (tools/standalone_llm_tool.py)
|
||||
- Bridges to external LLMs or a separate copilot service.
|
||||
- Functions: call_external_llm (uses OPENAI_API_KEY), call_external_copilot (uses COPILOT_API_URL).
|
||||
|
||||
- RepoIndexTool (tools/repo_index_tool.py) [NEW]
|
||||
- Quickly builds a lightweight index of repository paths via the GitHub Contents API to aid navigation, discovery, and targeted reads.
|
||||
- Functions:
|
||||
- get_repo_tree(path="", ref="main", max_depth=3, include_files=True, include_dirs=True)
|
||||
- find_files(pattern, path="", ref="main", max_results=50)
|
||||
- get_file_head(path, ref="main", max_bytes=4096)
|
||||
|
||||
## Adding a new tool
|
||||
|
||||
1) Create a new module under tools/ and subclass BaseTool.
|
||||
2) Implement get_functions() to describe your function signatures and parameters.
|
||||
3) Implement execute() to route to internal methods and return structured results or error strings.
|
||||
4) Prefer dependency injection and env vars over hardcoding. Reuse a shared requests.Session where practical.
|
||||
5) Log responsibly using the logging module; do not print directly. Attach a NullHandler by default to avoid handler warnings.
|
||||
6) Avoid storing secrets in memory; prefer short-lived per-request usage. Implement clear() to drop state.
|
||||
|
||||
## Environment variables (common)
|
||||
|
||||
- GITHUB_TOKEN: required for GitHub API access in most tools.
|
||||
- GITHUB_REPOSITORY: owner/repo used by GitHubTool and RepoIndexTool.
|
||||
- OPENAI_API_KEY: used by StandaloneLLMTool.
|
||||
- COPILOT_API_URL: used by StandaloneLLMTool for external copilot calls.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tools should be defensive and return clear error messages on failures.
|
||||
- Keep function results concise and JSON-serializable where possible.
|
||||
- If your tool fetches large data, consider pagination or size limits and expose parameters for control.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
from .base_tool import BaseTool
|
||||
import os
|
||||
import requests
|
||||
import base64
|
||||
import logging
|
||||
import fnmatch
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
class RepoIndexTool(BaseTool):
|
||||
"""
|
||||
Lightweight repository index and discovery helper using the GitHub Contents API.
|
||||
|
||||
Functions provided:
|
||||
- get_repo_tree(path="", ref="main", max_depth=3, include_files=True, include_dirs=True)
|
||||
- find_files(pattern, path="", ref="main", max_results=50)
|
||||
- get_file_head(path, ref="main", max_bytes=4096)
|
||||
"""
|
||||
|
||||
def __init__(self, session: Optional[requests.Session] = None, token: Optional[str] = None,
|
||||
repo: Optional[str] = None, base_url: Optional[str] = None, logger: Optional[logging.Logger] = None):
|
||||
self.base_url = base_url if base_url else "https://api.github.com"
|
||||
self._token = token if token else os.environ.get("GITHUB_TOKEN")
|
||||
self._repo = repo if repo else os.environ.get("GITHUB_REPOSITORY")
|
||||
|
||||
if not self._token:
|
||||
raise ValueError("GitHub token must be provided either as an argument or via GITHUB_TOKEN env var.")
|
||||
if not self._repo:
|
||||
raise ValueError("GitHub repository (e.g., 'owner/repo') must be provided either as an argument or via GITHUB_REPOSITORY env var.")
|
||||
|
||||
if session:
|
||||
self.session = session
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"Authorization": f"token {self._token}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
})
|
||||
|
||||
self.logger = logger if logger else logging.getLogger(__name__)
|
||||
if not self.logger.handlers:
|
||||
self.logger.addHandler(logging.NullHandler())
|
||||
|
||||
def clear(self):
|
||||
# No state to clear for this tool currently
|
||||
self.logger.debug("RepoIndexTool.clear called; no state to reset.")
|
||||
|
||||
def get_functions(self):
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_repo_tree",
|
||||
"description": "List repository paths under a directory with depth control via the Contents API.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path to start from (directory or file). Use '' or '.' for repo root.", "default": ""},
|
||||
"ref": {"type": "string", "description": "Branch, tag, or commit SHA to query.", "default": "main"},
|
||||
"max_depth": {"type": "integer", "description": "Maximum directory depth to recurse (0 = only the provided path).", "default": 3},
|
||||
"include_files": {"type": "boolean", "description": "Include file entries in results.", "default": True},
|
||||
"include_dirs": {"type": "boolean", "description": "Include directory entries in results.", "default": True}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"_tags": ["read"]
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "find_files",
|
||||
"description": "Find files matching a glob pattern starting under a path.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {"type": "string", "description": "Glob pattern (e.g., **/*.py, *.md) applied to full repository path."},
|
||||
"path": {"type": "string", "description": "Directory to start search from.", "default": ""},
|
||||
"ref": {"type": "string", "description": "Branch, tag, or commit SHA to query.", "default": "main"},
|
||||
"max_results": {"type": "integer", "description": "Maximum number of results to return.", "default": 50}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
}
|
||||
},
|
||||
"_tags": ["read"]
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_file_head",
|
||||
"description": "Return the first N bytes of a file (decoded as text) for a quick preview.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path to the file in the repository."},
|
||||
"ref": {"type": "string", "description": "Branch, tag, or commit SHA to query.", "default": "main"},
|
||||
"max_bytes": {"type": "integer", "description": "Maximum number of bytes from the beginning of the file to return.", "default": 4096}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
"_tags": ["read"]
|
||||
}
|
||||
]
|
||||
|
||||
def execute(self, function_name, **kwargs):
|
||||
self.logger.info(f"Executing RepoIndexTool function: {function_name} with args: {kwargs}")
|
||||
method_name = f"_{function_name}"
|
||||
if hasattr(self, method_name) and callable(getattr(self, method_name)):
|
||||
try:
|
||||
return getattr(self, method_name)(**kwargs)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error executing {function_name}: {e}", exc_info=True)
|
||||
return f"Error during {function_name} execution: {str(e)}"
|
||||
else:
|
||||
msg = f"Unknown function: {function_name}"
|
||||
self.logger.error(msg)
|
||||
return msg
|
||||
|
||||
# Private implementations
|
||||
|
||||
def _get_repo_tree(self, path: str = "", ref: str = "main", max_depth: int = 3,
|
||||
include_files: bool = True, include_dirs: bool = True):
|
||||
start_path = path.strip("/")
|
||||
if start_path in (".",):
|
||||
start_path = ""
|
||||
self.logger.info(f"Building repo tree from path='{start_path or '/'}', ref='{ref}', max_depth={max_depth}")
|
||||
results: List[Dict[str, Any]] = []
|
||||
try:
|
||||
self._collect_entries(start_path, ref, depth=max_depth, include_files=include_files, include_dirs=include_dirs, out=results)
|
||||
# Sort results for stability: directories first, then files, lexicographically by path
|
||||
results.sort(key=lambda e: (0 if e.get('type') == 'dir' else 1, e.get('path', '')))
|
||||
return results
|
||||
except requests.HTTPError as e:
|
||||
return f"HTTP error while listing '{start_path or '/'}' at '{ref}': {e.response.status_code} - {e.response.text if e.response is not None else e}"
|
||||
except Exception as e:
|
||||
return f"Error while building repo tree from '{start_path or '/'}' at '{ref}': {str(e)}"
|
||||
|
||||
def _collect_entries(self, path: str, ref: str, depth: int, include_files: bool, include_dirs: bool, out: List[Dict[str, Any]]):
|
||||
# Stop condition: when depth < 0, we do nothing; when depth == 0, we only include the node itself (if file/dir allowed)
|
||||
url = f"{self.base_url}/repos/{self._repo}/contents/{path}" if path else f"{self.base_url}/repos/{self._repo}/contents"
|
||||
params = {"ref": ref}
|
||||
self.logger.debug(f"Listing '{path or '/'}' with depth={depth}")
|
||||
resp = self.session.get(url, params=params)
|
||||
if resp.status_code == 404:
|
||||
self.logger.warning(f"Path not found: '{path or '/'}' at ref '{ref}'")
|
||||
return
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if isinstance(data, dict) and data.get('type') == 'file':
|
||||
if include_files:
|
||||
out.append(self._entry_from_item(data))
|
||||
return
|
||||
|
||||
# If it's a directory (list of items)
|
||||
if not isinstance(data, list):
|
||||
# Unexpected payload
|
||||
self.logger.debug(f"Unexpected payload for path '{path}': {type(data)}")
|
||||
return
|
||||
|
||||
# Add the directory itself if requested
|
||||
if include_dirs:
|
||||
out.append({
|
||||
"name": os.path.basename(path) if path else "/",
|
||||
"path": data[0]["path"].rsplit('/', 1)[0] if data else path, # derive path; if empty dir, fallback to path
|
||||
"type": "dir"
|
||||
})
|
||||
|
||||
if depth <= 0:
|
||||
return
|
||||
|
||||
for item in data:
|
||||
item_type = item.get('type')
|
||||
if item_type == 'file':
|
||||
if include_files:
|
||||
out.append(self._entry_from_item(item))
|
||||
elif item_type == 'dir':
|
||||
if depth > 0:
|
||||
# Recurse into subdir
|
||||
self._collect_entries(item.get('path', ''), ref, depth=depth - 1,
|
||||
include_files=include_files, include_dirs=include_dirs, out=out)
|
||||
else:
|
||||
# symlink, submodule, etc. We include a minimal record.
|
||||
out.append({"name": item.get('name'), "path": item.get('path'), "type": item_type or 'unknown'})
|
||||
|
||||
def _entry_from_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": item.get('name'),
|
||||
"path": item.get('path'),
|
||||
"type": item.get('type'),
|
||||
"size": item.get('size'),
|
||||
"sha": item.get('sha')
|
||||
}
|
||||
|
||||
def _find_files(self, pattern: str, path: str = "", ref: str = "main", max_results: int = 50):
|
||||
# Build a limited-depth tree; for search, default to a moderate depth to avoid huge traversals.
|
||||
# Here we use a depth of 10 to allow most repos; adjust if needed via path segmentation.
|
||||
self.logger.info(f"Finding files matching pattern='{pattern}' under '{path or '/'}' at ref '{ref}' (max_results={max_results})")
|
||||
tree = self._get_repo_tree(path=path, ref=ref, max_depth=10, include_files=True, include_dirs=False)
|
||||
if isinstance(tree, str):
|
||||
return tree # error string
|
||||
matches = []
|
||||
for entry in tree:
|
||||
p = entry.get('path', '')
|
||||
if entry.get('type') == 'file' and fnmatch.fnmatch(p, pattern):
|
||||
matches.append(entry)
|
||||
if len(matches) >= max_results:
|
||||
break
|
||||
return matches
|
||||
|
||||
def _get_file_head(self, path: str, ref: str = "main", max_bytes: int = 4096):
|
||||
self.logger.info(f"Fetching head of file '{path}' at ref '{ref}', up to {max_bytes} bytes")
|
||||
url = f"{self.base_url}/repos/{self._repo}/contents/{path}"
|
||||
resp = self.session.get(url, params={"ref": ref})
|
||||
if resp.status_code == 404:
|
||||
return f"Error: File '{path}' not found at ref '{ref}'."
|
||||
if resp.status_code != 200:
|
||||
return f"Error reading file '{path}' at ref '{ref}': {resp.status_code} - {resp.text}"
|
||||
data = resp.json()
|
||||
if data.get('type') != 'file':
|
||||
return f"Error: Path '{path}' is not a file."
|
||||
try:
|
||||
content_b64 = data.get('content', '')
|
||||
decoded = base64.b64decode(content_b64).decode('utf-8', errors='replace')
|
||||
return decoded[:max_bytes]
|
||||
except Exception as e:
|
||||
return f"Error decoding content for '{path}': {str(e)}"
|
||||
Reference in New Issue
Block a user