Compare commits

...

10 Commits

Author SHA1 Message Date
admin c0cfeb2454 Github agent addition
Python CI / test (3.10) (push) Waiting to run
2026-05-30 16:30:25 -05:00
admin 65537c4174 removed inference limit code 2026-01-21 13:41:03 -06:00
admin b29cd6e6f6 Merge branch 'main' of https://github.com/bucolucas/cyclop 2025-08-13 14:32:00 -05:00
admin 30b2fcc66f Updated developer prompt 2025-08-13 14:31:35 -05:00
admin 124a09def1 Merge pull request #237 from bucolucas/feat/context-window-optimizer-16k-32k
feat: 16k/32k context management with request-only tool-call summarization and budget enforcement
2025-08-13 14:31:20 -05:00
admin b9b07320bc feat: robust 16k/32k context management with request-only tool-call summarization and budget enforcement
- Add normalization of messages before API calls
- Implement token projection and enforce budget for 16k/32k windows
- Summarize only tool-call request arguments (not responses) when over budget
- Optionally elide redundant code blocks in old assistant messages as last-resort trimming
- Default small-model limit to 16k, large to 32k; reserve space for response tokens
- Keep core behavior and tool execution unchanged
2025-08-13 14:25:13 -05:00
admin 7bd1bcf82b Fixed code errors 2025-08-13 13:36:03 -05:00
admin a820099e3e Merge pull request #235 from bucolucas/docs/add-tools-readme
Add RepoIndexTool and expand tools README
2025-08-09 18:42:19 -05:00
admin 6d7ec9f84b feat(tools): add RepoIndexTool for fast repo discovery and content previews 2025-08-09 18:36:36 -05:00
admin b6fd77e0be Docs: add comprehensive tools overview and contribution guide 2025-08-09 18:35:27 -05:00
5 changed files with 524 additions and 116 deletions
+175 -41
View File
@@ -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")
+54 -75
View File
@@ -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.
+11
View File
@@ -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.
+57
View File
@@ -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.
+227
View File
@@ -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)}"