docugraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docugraph/__init__.py +24 -0
- docugraph/_version.py +28 -0
- docugraph/agents/__init__.py +27 -0
- docugraph/agents/coding_agent.py +532 -0
- docugraph/agents/tools/__init__.py +62 -0
- docugraph/agents/tools/graph_query.py +203 -0
- docugraph/agents/tools/memory_ops.py +305 -0
- docugraph/agents/tools/search_code.py +305 -0
- docugraph/agents/tools/search_docs.py +148 -0
- docugraph/core/__init__.py +34 -0
- docugraph/core/config.py +183 -0
- docugraph/core/embeddings.py +394 -0
- docugraph/core/llm.py +892 -0
- docugraph/core/models.py +145 -0
- docugraph/ingestion/__init__.py +64 -0
- docugraph/ingestion/api_docs.py +646 -0
- docugraph/ingestion/chunker.py +382 -0
- docugraph/ingestion/crawler.py +305 -0
- docugraph/ingestion/git_indexer.py +556 -0
- docugraph/ingestion/local_files.py +516 -0
- docugraph/interfaces/__init__.py +1 -0
- docugraph/interfaces/cli.py +983 -0
- docugraph/interfaces/mcp_server.py +918 -0
- docugraph/retrieval/__init__.py +54 -0
- docugraph/retrieval/context_builder.py +522 -0
- docugraph/retrieval/hybrid_search.py +534 -0
- docugraph/retrieval/reranker.py +532 -0
- docugraph/storage/__init__.py +14 -0
- docugraph/storage/graph_store.py +602 -0
- docugraph/storage/memory_store.py +529 -0
- docugraph/storage/vector_store.py +367 -0
- docugraph-0.1.0.dist-info/METADATA +354 -0
- docugraph-0.1.0.dist-info/RECORD +37 -0
- docugraph-0.1.0.dist-info/WHEEL +4 -0
- docugraph-0.1.0.dist-info/entry_points.txt +3 -0
- docugraph-0.1.0.dist-info/licenses/LICENSE +201 -0
- docugraph-0.1.0.dist-info/licenses/NOTICE +10 -0
docugraph/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DocuGraph AI - Intelligent coding assistant with knowledge graphs and vector search.
|
|
3
|
+
|
|
4
|
+
A powerful coding agent platform combining:
|
|
5
|
+
- Knowledge graphs (Graphiti + Kuzu)
|
|
6
|
+
- Vector search (LanceDB)
|
|
7
|
+
- Web crawling (Crawl4AI)
|
|
8
|
+
- Agent memory (MCP-compatible)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from docugraph._version import __version__
|
|
12
|
+
from docugraph.core.config import Config, get_config
|
|
13
|
+
from docugraph.core.models import Chunk, Document, Entity, MemoryEntry, Relationship
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"Config",
|
|
18
|
+
"get_config",
|
|
19
|
+
"Document",
|
|
20
|
+
"Chunk",
|
|
21
|
+
"Entity",
|
|
22
|
+
"Relationship",
|
|
23
|
+
"MemoryEntry",
|
|
24
|
+
]
|
docugraph/_version.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The package version, read from installed distribution metadata.
|
|
2
|
+
|
|
3
|
+
`pyproject.toml` is the one home for the version number. Everything that
|
|
4
|
+
reports it -- ``__version__``, the CLI's ``--version``, the MCP handshake's
|
|
5
|
+
``serverInfo.version``, and the crawler's User-Agent -- reads it from here,
|
|
6
|
+
so the number cannot drift into a second place and start disagreeing with
|
|
7
|
+
itself. (It did: the distribution said 1.0.0, the CLI said 0.1.0, and the MCP
|
|
8
|
+
handshake reported the *mcp SDK's* version because the server was constructed
|
|
9
|
+
without one.)
|
|
10
|
+
|
|
11
|
+
``DISTRIBUTION_NAME`` must match ``name`` in pyproject.toml; a test asserts
|
|
12
|
+
that, so renaming the package fails loudly here instead of silently falling
|
|
13
|
+
back to the placeholder below.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
DISTRIBUTION_NAME = "docugraph"
|
|
19
|
+
|
|
20
|
+
_UNKNOWN = "0.0.0.dev0"
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
__version__ = version(DISTRIBUTION_NAME)
|
|
24
|
+
except PackageNotFoundError: # pragma: no cover -- source tree, never installed
|
|
25
|
+
# Running from a checkout that was never installed (no metadata to read).
|
|
26
|
+
# A placeholder is better than crashing on import, and better than a
|
|
27
|
+
# plausible-looking number that would be a lie.
|
|
28
|
+
__version__ = _UNKNOWN
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent orchestration and tool implementations.
|
|
2
|
+
|
|
3
|
+
The agents module provides:
|
|
4
|
+
- CodingAgent: Main agent for code assistance with RAG
|
|
5
|
+
- Tools: Modular tools for search, graph queries, and memory
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from docugraph.agents.coding_agent import (
|
|
9
|
+
AgentAction,
|
|
10
|
+
AgentConfig,
|
|
11
|
+
AgentContext,
|
|
12
|
+
AgentMessage,
|
|
13
|
+
CodingAgent,
|
|
14
|
+
ToolResult,
|
|
15
|
+
get_coding_agent,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Agent
|
|
20
|
+
"CodingAgent",
|
|
21
|
+
"AgentConfig",
|
|
22
|
+
"AgentContext",
|
|
23
|
+
"AgentMessage",
|
|
24
|
+
"AgentAction",
|
|
25
|
+
"ToolResult",
|
|
26
|
+
"get_coding_agent",
|
|
27
|
+
]
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""Main coding agent orchestration.
|
|
2
|
+
|
|
3
|
+
The CodingAgent provides intelligent code assistance by:
|
|
4
|
+
- Searching indexed documentation
|
|
5
|
+
- Querying knowledge graphs for relationships
|
|
6
|
+
- Maintaining session memory for context
|
|
7
|
+
- Analyzing code structure
|
|
8
|
+
|
|
9
|
+
LLM-agnostic: Works with Ollama (local), OpenAI, or Anthropic.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from docugraph.core.llm import LLMProviderBase, get_llm_provider
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentAction(Enum):
|
|
25
|
+
"""Actions the agent can take."""
|
|
26
|
+
|
|
27
|
+
SEARCH_DOCS = "search_docs"
|
|
28
|
+
SEARCH_CODE = "search_code"
|
|
29
|
+
GRAPH_QUERY = "graph_query"
|
|
30
|
+
MEMORY_STORE = "memory_store"
|
|
31
|
+
MEMORY_RECALL = "memory_recall"
|
|
32
|
+
RESPOND = "respond"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AgentContext:
|
|
37
|
+
"""Context for agent execution."""
|
|
38
|
+
|
|
39
|
+
repository: str = "default"
|
|
40
|
+
branch: str = "main"
|
|
41
|
+
session_id: str | None = None
|
|
42
|
+
working_directory: str | None = None
|
|
43
|
+
language: str | None = None # Primary programming language
|
|
44
|
+
framework: str | None = None # Primary framework
|
|
45
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AgentMessage:
|
|
50
|
+
"""A message in the agent conversation."""
|
|
51
|
+
|
|
52
|
+
role: str # "user", "assistant", "system", "tool"
|
|
53
|
+
content: str
|
|
54
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
55
|
+
tool_name: str | None = None
|
|
56
|
+
tool_result: Any = None
|
|
57
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ToolResult:
|
|
62
|
+
"""Result from a tool execution."""
|
|
63
|
+
|
|
64
|
+
success: bool
|
|
65
|
+
data: Any
|
|
66
|
+
error: str | None = None
|
|
67
|
+
took_ms: float = 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class AgentConfig:
|
|
72
|
+
"""Configuration for the coding agent."""
|
|
73
|
+
|
|
74
|
+
# LLM settings
|
|
75
|
+
llm_temperature: float = 0.1
|
|
76
|
+
llm_max_tokens: int = 4000
|
|
77
|
+
|
|
78
|
+
# Search settings
|
|
79
|
+
search_top_k: int = 5
|
|
80
|
+
code_search_top_k: int = 10
|
|
81
|
+
|
|
82
|
+
# Memory settings
|
|
83
|
+
max_context_messages: int = 20
|
|
84
|
+
include_memory_context: bool = True
|
|
85
|
+
|
|
86
|
+
# Behavior
|
|
87
|
+
auto_search_threshold: float = 0.7 # Confidence to auto-search
|
|
88
|
+
verbose: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CodingAgent:
|
|
92
|
+
"""Intelligent coding assistant with RAG capabilities.
|
|
93
|
+
|
|
94
|
+
Combines multiple retrieval methods to provide contextual assistance:
|
|
95
|
+
- Vector search over indexed documentation
|
|
96
|
+
- Knowledge graph queries for relationships
|
|
97
|
+
- Session memory for conversation context
|
|
98
|
+
- Code analysis for structure understanding
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
agent = CodingAgent()
|
|
102
|
+
response = await agent.query("How do I use asyncio.gather?")
|
|
103
|
+
print(response)
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
config: AgentConfig | None = None,
|
|
109
|
+
context: AgentContext | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Initialize the coding agent.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Agent configuration
|
|
115
|
+
context: Initial execution context
|
|
116
|
+
"""
|
|
117
|
+
self._config = config or AgentConfig()
|
|
118
|
+
self._context = context or AgentContext()
|
|
119
|
+
self._messages: list[AgentMessage] = []
|
|
120
|
+
self._llm: LLMProviderBase | None = None
|
|
121
|
+
self._tools: dict[str, Callable[..., Any]] = {}
|
|
122
|
+
|
|
123
|
+
# Register default tools
|
|
124
|
+
self._register_default_tools()
|
|
125
|
+
|
|
126
|
+
def _get_llm(self) -> LLMProviderBase:
|
|
127
|
+
"""Lazy load LLM provider."""
|
|
128
|
+
if self._llm is None:
|
|
129
|
+
self._llm = get_llm_provider()
|
|
130
|
+
return self._llm
|
|
131
|
+
|
|
132
|
+
def _register_default_tools(self) -> None:
|
|
133
|
+
"""Register the default tools."""
|
|
134
|
+
# Import via explicit submodule paths — the tools package re-exports
|
|
135
|
+
# same-named functions that shadow the module names.
|
|
136
|
+
from docugraph.agents.tools.graph_query import graph_query
|
|
137
|
+
from docugraph.agents.tools.memory_ops import memory_recall, memory_store
|
|
138
|
+
from docugraph.agents.tools.search_code import search_code_async
|
|
139
|
+
from docugraph.agents.tools.search_docs import search_docs_async
|
|
140
|
+
|
|
141
|
+
# The async implementations: this loop is async and `execute_tool`
|
|
142
|
+
# awaits coroutine functions, so a sync wrapper that drives its own
|
|
143
|
+
# loop would raise here.
|
|
144
|
+
self._tools = {
|
|
145
|
+
"search_docs": search_docs_async,
|
|
146
|
+
"search_code": search_code_async,
|
|
147
|
+
"graph_query": graph_query,
|
|
148
|
+
"memory_store": memory_store,
|
|
149
|
+
"memory_recall": memory_recall,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def register_tool(self, name: str, func: Callable[..., Any]) -> None:
|
|
153
|
+
"""Register a custom tool.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
name: Tool name
|
|
157
|
+
func: Tool function (sync or async)
|
|
158
|
+
"""
|
|
159
|
+
self._tools[name] = func
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def context(self) -> AgentContext:
|
|
163
|
+
"""Get current execution context."""
|
|
164
|
+
return self._context
|
|
165
|
+
|
|
166
|
+
@context.setter
|
|
167
|
+
def context(self, ctx: AgentContext) -> None:
|
|
168
|
+
"""Set execution context."""
|
|
169
|
+
self._context = ctx
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def messages(self) -> list[AgentMessage]:
|
|
173
|
+
"""Get conversation messages."""
|
|
174
|
+
return self._messages.copy()
|
|
175
|
+
|
|
176
|
+
def add_message(self, role: str, content: str, **kwargs: Any) -> None:
|
|
177
|
+
"""Add a message to the conversation.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
role: Message role (user, assistant, system, tool)
|
|
181
|
+
content: Message content
|
|
182
|
+
**kwargs: Additional message metadata
|
|
183
|
+
"""
|
|
184
|
+
self._messages.append(
|
|
185
|
+
AgentMessage(
|
|
186
|
+
role=role,
|
|
187
|
+
content=content,
|
|
188
|
+
**kwargs,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Trim old messages if needed
|
|
193
|
+
if len(self._messages) > self._config.max_context_messages:
|
|
194
|
+
# Keep system messages and recent messages
|
|
195
|
+
system_msgs = [m for m in self._messages if m.role == "system"]
|
|
196
|
+
other_msgs = [m for m in self._messages if m.role != "system"]
|
|
197
|
+
keep_count = self._config.max_context_messages - len(system_msgs)
|
|
198
|
+
self._messages = system_msgs + other_msgs[-keep_count:]
|
|
199
|
+
|
|
200
|
+
def clear_messages(self) -> None:
|
|
201
|
+
"""Clear conversation history."""
|
|
202
|
+
self._messages.clear()
|
|
203
|
+
|
|
204
|
+
async def execute_tool(
|
|
205
|
+
self,
|
|
206
|
+
tool_name: str,
|
|
207
|
+
arguments: dict[str, Any],
|
|
208
|
+
) -> ToolResult:
|
|
209
|
+
"""Execute a tool by name.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
tool_name: Name of the tool to execute
|
|
213
|
+
arguments: Tool arguments
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Tool execution result
|
|
217
|
+
"""
|
|
218
|
+
import time
|
|
219
|
+
|
|
220
|
+
if tool_name not in self._tools:
|
|
221
|
+
return ToolResult(
|
|
222
|
+
success=False,
|
|
223
|
+
data=None,
|
|
224
|
+
error=f"Unknown tool: {tool_name}",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
start_time = time.time()
|
|
228
|
+
tool_func = self._tools[tool_name]
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Add context to arguments
|
|
232
|
+
arguments["_context"] = self._context
|
|
233
|
+
|
|
234
|
+
# Check if async
|
|
235
|
+
import asyncio
|
|
236
|
+
|
|
237
|
+
if asyncio.iscoroutinefunction(tool_func):
|
|
238
|
+
result = await tool_func(**arguments)
|
|
239
|
+
else:
|
|
240
|
+
result = tool_func(**arguments)
|
|
241
|
+
|
|
242
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
243
|
+
|
|
244
|
+
return ToolResult(
|
|
245
|
+
success=True,
|
|
246
|
+
data=result,
|
|
247
|
+
took_ms=elapsed_ms,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
252
|
+
return ToolResult(
|
|
253
|
+
success=False,
|
|
254
|
+
data=None,
|
|
255
|
+
error=str(e),
|
|
256
|
+
took_ms=elapsed_ms,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _build_system_prompt(self) -> str:
|
|
260
|
+
"""Build the system prompt for the agent."""
|
|
261
|
+
tools_desc = []
|
|
262
|
+
for name in self._tools:
|
|
263
|
+
if name == "search_docs":
|
|
264
|
+
tools_desc.append("- search_docs(query, top_k): Search indexed documentation")
|
|
265
|
+
elif name == "search_code":
|
|
266
|
+
tools_desc.append("- search_code(query, language, top_k): Search for code examples")
|
|
267
|
+
elif name == "graph_query":
|
|
268
|
+
tools_desc.append("- graph_query(query): Query knowledge graph for relationships")
|
|
269
|
+
elif name == "memory_store":
|
|
270
|
+
tools_desc.append("- memory_store(key, value): Store information for later")
|
|
271
|
+
elif name == "memory_recall":
|
|
272
|
+
tools_desc.append("- memory_recall(key): Retrieve stored information")
|
|
273
|
+
|
|
274
|
+
return f"""You are an intelligent coding assistant with access to indexed documentation and a knowledge graph.
|
|
275
|
+
|
|
276
|
+
Your capabilities:
|
|
277
|
+
{chr(10).join(tools_desc)}
|
|
278
|
+
|
|
279
|
+
Context:
|
|
280
|
+
- Repository: {self._context.repository}
|
|
281
|
+
- Branch: {self._context.branch}
|
|
282
|
+
- Language: {self._context.language or "Not specified"}
|
|
283
|
+
- Framework: {self._context.framework or "Not specified"}
|
|
284
|
+
|
|
285
|
+
Guidelines:
|
|
286
|
+
1. Search documentation before answering technical questions
|
|
287
|
+
2. Use the knowledge graph for understanding relationships between concepts
|
|
288
|
+
3. Store important decisions and context in memory for future reference
|
|
289
|
+
4. Provide clear, accurate answers with references to sources
|
|
290
|
+
5. If unsure, acknowledge uncertainty and suggest where to find more information
|
|
291
|
+
|
|
292
|
+
When you need to use a tool, respond with a JSON object:
|
|
293
|
+
{{"tool": "tool_name", "arguments": {{"arg1": "value1"}}}}
|
|
294
|
+
|
|
295
|
+
After receiving tool results, synthesize the information into a helpful response."""
|
|
296
|
+
|
|
297
|
+
async def _decide_action(self, user_input: str) -> tuple[AgentAction, dict[str, Any]]:
|
|
298
|
+
"""Decide what action to take based on user input.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
user_input: User's message
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tuple of (action, arguments)
|
|
305
|
+
"""
|
|
306
|
+
# Simple heuristics for action selection
|
|
307
|
+
user_lower = user_input.lower()
|
|
308
|
+
|
|
309
|
+
# Memory-related queries
|
|
310
|
+
if any(kw in user_lower for kw in ["remember", "recall", "what did"]):
|
|
311
|
+
return AgentAction.MEMORY_RECALL, {"key": "*"}
|
|
312
|
+
|
|
313
|
+
# Explicit search requests
|
|
314
|
+
if any(kw in user_lower for kw in ["search", "find", "look up", "how to", "what is"]):
|
|
315
|
+
return AgentAction.SEARCH_DOCS, {"query": user_input}
|
|
316
|
+
|
|
317
|
+
# Code-related queries
|
|
318
|
+
if any(kw in user_lower for kw in ["code example", "show code", "implement", "function"]):
|
|
319
|
+
return AgentAction.SEARCH_CODE, {"query": user_input}
|
|
320
|
+
|
|
321
|
+
# Relationship queries
|
|
322
|
+
if any(kw in user_lower for kw in ["related to", "depends on", "uses", "relationship"]):
|
|
323
|
+
return AgentAction.GRAPH_QUERY, {"query": user_input}
|
|
324
|
+
|
|
325
|
+
# Default: Search docs for technical questions
|
|
326
|
+
if "?" in user_input or any(kw in user_lower for kw in ["explain", "describe", "tell me"]):
|
|
327
|
+
return AgentAction.SEARCH_DOCS, {"query": user_input}
|
|
328
|
+
|
|
329
|
+
# Otherwise, respond directly
|
|
330
|
+
return AgentAction.RESPOND, {}
|
|
331
|
+
|
|
332
|
+
async def query(
|
|
333
|
+
self,
|
|
334
|
+
user_input: str,
|
|
335
|
+
auto_search: bool = True,
|
|
336
|
+
) -> str:
|
|
337
|
+
"""Query the agent with a user message.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
user_input: User's question or request
|
|
341
|
+
auto_search: Automatically search if relevant
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Agent's response
|
|
345
|
+
"""
|
|
346
|
+
# Add user message
|
|
347
|
+
self.add_message("user", user_input)
|
|
348
|
+
|
|
349
|
+
# Decide action
|
|
350
|
+
action, action_args = await self._decide_action(user_input)
|
|
351
|
+
|
|
352
|
+
tool_context = ""
|
|
353
|
+
|
|
354
|
+
if action != AgentAction.RESPOND and auto_search:
|
|
355
|
+
# Execute the relevant tool
|
|
356
|
+
tool_name = action.value
|
|
357
|
+
result = await self.execute_tool(tool_name, action_args)
|
|
358
|
+
|
|
359
|
+
if result.success and result.data:
|
|
360
|
+
tool_context = self._format_tool_result(tool_name, result.data)
|
|
361
|
+
|
|
362
|
+
# Add tool message
|
|
363
|
+
self.add_message(
|
|
364
|
+
"tool",
|
|
365
|
+
tool_context,
|
|
366
|
+
tool_name=tool_name,
|
|
367
|
+
tool_result=result.data,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Build prompt for LLM
|
|
371
|
+
system_prompt = self._build_system_prompt()
|
|
372
|
+
|
|
373
|
+
# Build conversation context
|
|
374
|
+
messages_context = []
|
|
375
|
+
for msg in self._messages[-self._config.max_context_messages :]:
|
|
376
|
+
if msg.role == "user":
|
|
377
|
+
messages_context.append(f"User: {msg.content}")
|
|
378
|
+
elif msg.role == "assistant":
|
|
379
|
+
messages_context.append(f"Assistant: {msg.content}")
|
|
380
|
+
elif msg.role == "tool":
|
|
381
|
+
messages_context.append(f"[Tool Result: {msg.tool_name}]\n{msg.content}")
|
|
382
|
+
|
|
383
|
+
full_prompt = f"""Based on the conversation and any tool results, provide a helpful response.
|
|
384
|
+
|
|
385
|
+
{chr(10).join(messages_context)}
|
|
386
|
+
|
|
387
|
+
Provide a clear, helpful response to the user's question."""
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
llm = self._get_llm()
|
|
391
|
+
response = await llm.generate(
|
|
392
|
+
prompt=full_prompt,
|
|
393
|
+
system_prompt=system_prompt,
|
|
394
|
+
temperature=self._config.llm_temperature,
|
|
395
|
+
max_tokens=self._config.llm_max_tokens,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Check if LLM wants to use a tool
|
|
399
|
+
response = response.strip()
|
|
400
|
+
if response.startswith("{") and '"tool"' in response:
|
|
401
|
+
try:
|
|
402
|
+
tool_call = json.loads(response)
|
|
403
|
+
if "tool" in tool_call and "arguments" in tool_call:
|
|
404
|
+
tool_result = await self.execute_tool(
|
|
405
|
+
tool_call["tool"],
|
|
406
|
+
tool_call["arguments"],
|
|
407
|
+
)
|
|
408
|
+
if tool_result.success:
|
|
409
|
+
# Add tool result and re-query
|
|
410
|
+
tool_context = self._format_tool_result(
|
|
411
|
+
tool_call["tool"],
|
|
412
|
+
tool_result.data,
|
|
413
|
+
)
|
|
414
|
+
self.add_message(
|
|
415
|
+
"tool",
|
|
416
|
+
tool_context,
|
|
417
|
+
tool_name=tool_call["tool"],
|
|
418
|
+
tool_result=tool_result.data,
|
|
419
|
+
)
|
|
420
|
+
# Get final response
|
|
421
|
+
response = await llm.generate(
|
|
422
|
+
prompt=f"{full_prompt}\n\n[Tool Result]\n{tool_context}\n\nNow provide the final response:",
|
|
423
|
+
system_prompt=system_prompt,
|
|
424
|
+
temperature=self._config.llm_temperature,
|
|
425
|
+
max_tokens=self._config.llm_max_tokens,
|
|
426
|
+
)
|
|
427
|
+
except json.JSONDecodeError:
|
|
428
|
+
pass # Not a tool call
|
|
429
|
+
|
|
430
|
+
# Add assistant response
|
|
431
|
+
self.add_message("assistant", response)
|
|
432
|
+
|
|
433
|
+
return response
|
|
434
|
+
|
|
435
|
+
except Exception as e:
|
|
436
|
+
error_msg = f"Error generating response: {str(e)}"
|
|
437
|
+
self.add_message("assistant", error_msg)
|
|
438
|
+
return error_msg
|
|
439
|
+
|
|
440
|
+
def _format_tool_result(self, tool_name: str, data: Any) -> str:
|
|
441
|
+
"""Format tool result for context.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
tool_name: Name of the tool
|
|
445
|
+
data: Tool result data
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Formatted string
|
|
449
|
+
"""
|
|
450
|
+
if isinstance(data, list):
|
|
451
|
+
if tool_name == "search_docs":
|
|
452
|
+
# Format search results
|
|
453
|
+
parts = []
|
|
454
|
+
for i, item in enumerate(data[:5], 1):
|
|
455
|
+
if isinstance(item, dict):
|
|
456
|
+
content = item.get("content", "")[:500]
|
|
457
|
+
source = item.get("source", "Unknown")
|
|
458
|
+
score = item.get("score", 0)
|
|
459
|
+
parts.append(f"[{i}] (score: {score:.2f}) {source}\n{content}")
|
|
460
|
+
else:
|
|
461
|
+
parts.append(f"[{i}] {str(item)[:500]}")
|
|
462
|
+
return "\n\n".join(parts)
|
|
463
|
+
|
|
464
|
+
elif tool_name == "search_code":
|
|
465
|
+
# Format code results
|
|
466
|
+
parts = []
|
|
467
|
+
for i, item in enumerate(data[:5], 1):
|
|
468
|
+
if isinstance(item, dict):
|
|
469
|
+
code = item.get("code", "")[:1000]
|
|
470
|
+
path = item.get("path", "Unknown")
|
|
471
|
+
lang = item.get("language", "")
|
|
472
|
+
parts.append(f"[{i}] {path}\n```{lang}\n{code}\n```")
|
|
473
|
+
else:
|
|
474
|
+
parts.append(f"[{i}] {str(item)[:500]}")
|
|
475
|
+
return "\n\n".join(parts)
|
|
476
|
+
|
|
477
|
+
elif tool_name == "graph_query":
|
|
478
|
+
# Format graph results
|
|
479
|
+
parts = []
|
|
480
|
+
for item in data[:10]:
|
|
481
|
+
if isinstance(item, dict):
|
|
482
|
+
fact = item.get("fact", str(item))
|
|
483
|
+
parts.append(f"- {fact}")
|
|
484
|
+
else:
|
|
485
|
+
parts.append(f"- {str(item)}")
|
|
486
|
+
return "\n".join(parts)
|
|
487
|
+
|
|
488
|
+
else:
|
|
489
|
+
return json.dumps(data, indent=2, default=str)[:2000]
|
|
490
|
+
|
|
491
|
+
elif isinstance(data, dict):
|
|
492
|
+
return json.dumps(data, indent=2, default=str)[:2000]
|
|
493
|
+
|
|
494
|
+
else:
|
|
495
|
+
return str(data)[:2000]
|
|
496
|
+
|
|
497
|
+
async def summarize_session(self) -> str:
|
|
498
|
+
"""Summarize the current session.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Summary of the conversation
|
|
502
|
+
"""
|
|
503
|
+
if not self._messages:
|
|
504
|
+
return "No messages in session."
|
|
505
|
+
|
|
506
|
+
user_questions = [m.content for m in self._messages if m.role == "user"]
|
|
507
|
+
tool_calls = [m.tool_name for m in self._messages if m.tool_name]
|
|
508
|
+
|
|
509
|
+
summary_parts = [
|
|
510
|
+
f"Session with {len(self._messages)} messages",
|
|
511
|
+
f"Repository: {self._context.repository}/{self._context.branch}",
|
|
512
|
+
f"Questions asked: {len(user_questions)}",
|
|
513
|
+
f"Tools used: {', '.join(set(tool_calls)) or 'None'}",
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
return "\n".join(summary_parts)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def get_coding_agent(
|
|
520
|
+
config: AgentConfig | None = None,
|
|
521
|
+
context: AgentContext | None = None,
|
|
522
|
+
) -> CodingAgent:
|
|
523
|
+
"""Factory function to get a CodingAgent instance.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
config: Optional agent configuration
|
|
527
|
+
context: Optional execution context
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
CodingAgent instance
|
|
531
|
+
"""
|
|
532
|
+
return CodingAgent(config=config, context=context)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Agent tools for the coding agent.
|
|
2
|
+
|
|
3
|
+
Provides modular tools for:
|
|
4
|
+
- Documentation search (vector and hybrid)
|
|
5
|
+
- Code search with language awareness
|
|
6
|
+
- Knowledge graph queries
|
|
7
|
+
- Session memory operations
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from docugraph.agents.tools.graph_query import (
|
|
11
|
+
find_related_entities,
|
|
12
|
+
get_entity_context,
|
|
13
|
+
graph_add_content,
|
|
14
|
+
graph_query,
|
|
15
|
+
query_entities_sync,
|
|
16
|
+
)
|
|
17
|
+
from docugraph.agents.tools.memory_ops import (
|
|
18
|
+
memory_delete,
|
|
19
|
+
memory_recall,
|
|
20
|
+
memory_search,
|
|
21
|
+
memory_store,
|
|
22
|
+
recall_context,
|
|
23
|
+
recall_decisions,
|
|
24
|
+
store_context,
|
|
25
|
+
store_decision,
|
|
26
|
+
)
|
|
27
|
+
from docugraph.agents.tools.search_code import (
|
|
28
|
+
search_code,
|
|
29
|
+
search_code_async,
|
|
30
|
+
search_symbols,
|
|
31
|
+
)
|
|
32
|
+
from docugraph.agents.tools.search_docs import (
|
|
33
|
+
search_by_source,
|
|
34
|
+
search_docs,
|
|
35
|
+
search_docs_async,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Search docs
|
|
40
|
+
"search_docs",
|
|
41
|
+
"search_docs_async",
|
|
42
|
+
"search_by_source",
|
|
43
|
+
# Search code
|
|
44
|
+
"search_code",
|
|
45
|
+
"search_code_async",
|
|
46
|
+
"search_symbols",
|
|
47
|
+
# Graph query
|
|
48
|
+
"graph_query",
|
|
49
|
+
"graph_add_content",
|
|
50
|
+
"find_related_entities",
|
|
51
|
+
"get_entity_context",
|
|
52
|
+
"query_entities_sync",
|
|
53
|
+
# Memory ops
|
|
54
|
+
"memory_store",
|
|
55
|
+
"memory_recall",
|
|
56
|
+
"memory_delete",
|
|
57
|
+
"memory_search",
|
|
58
|
+
"store_decision",
|
|
59
|
+
"recall_decisions",
|
|
60
|
+
"store_context",
|
|
61
|
+
"recall_context",
|
|
62
|
+
]
|