Skip to content

Commit

Permalink
add module explorer tool
Browse files Browse the repository at this point in the history
  • Loading branch information
zzstoatzz committed Dec 22, 2024
1 parent be497d9 commit 4ec7b3f
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 76 deletions.
6 changes: 4 additions & 2 deletions cookbook/slackbot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,17 @@ async def handle_message(payload: SlackPayload, db: Database):
return Completed(message="Message too long", name="SKIPPED")

if re.search(BOT_MENTION, user_message) and payload.authorizations:
logger.info(f"Processing message in thread {thread_ts}")
logger.info(
f"Processing message in thread {thread_ts}\nUser message: {cleaned_message}"
)
conversation = await db.get_thread_messages(thread_ts)

user_context = build_user_context(
user_id=payload.authorizations[0].user_id,
user_question=cleaned_message,
)

result = await task(agent.run)(
result = await flow(agent.run)(
user_prompt=cleaned_message,
message_history=conversation,
deps=user_context,
Expand Down
13 changes: 11 additions & 2 deletions cookbook/slackbot/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from raggy.documents import Document
from raggy.vectorstores.tpuf import TurboPuffer, multi_query_tpuf
from search import (
explore_module_offerings,
get_latest_prefect_release_notes,
review_common_3x_gotchas,
review_top_level_prefect_api,
search_controlflow_docs,
search_prefect_2x_docs,
search_prefect_3x_docs,
Expand All @@ -38,11 +41,14 @@
USE TOOLS REPEATEDLY to gather context from the docs, github issues or other tools.
Any notes you take about the user will be automatically stored for your next interaction with them.
Assume no knowledge of Prefect syntax without reading docs. ALWAYS include relevant links from tool outputs.
Always review the top level API of Prefect before offering code examples to avoid offering fake imports.
Generally, follow this pattern while generating each response:
1) If user offers info about their stack or objectives -> store relevant facts and continue to following steps
2) Use tools to gather context about Prefect concepts related to their question
3) Compile relevant facts and context into a single, CONCISE answer
4) If user asks a follow-up question, repeat steps 2-3
3) Review the top level API of Prefect and drill into submodules that may be related to the user's question
4) Compile relevant facts and context into a single, CONCISE answer
5) If user asks a follow-up question, repeat steps 2-3
NEVER reference features, syntax, imports or env vars that you do not explicitly find in the docs.
If not explicitly stated, assume that the user is using Prefect 3.x and vocalize this assumption.
If asked an ambiguous question, simply state what you know about the user and your capabilities."""
Expand Down Expand Up @@ -187,6 +193,9 @@ def create_agent(
search_prefect_3x_docs,
search_controlflow_docs,
read_github_issues,
review_top_level_prefect_api,
explore_module_offerings,
review_common_3x_gotchas,
],
deps_type=UserContext,
)
Expand Down
190 changes: 190 additions & 0 deletions cookbook/slackbot/modules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import importlib
import inspect
import pkgutil
from types import ModuleType
from typing import Any


class ModuleTreeExplorer:
def __init__(self, root_module_path: str, max_depth: int = 2):
"""
Initialize the module tree explorer with a root module path.
Args:
root_module_path: String representing the root module (e.g., 'prefect.runtime')
max_depth: Maximum depth to explore in the module tree (default: 2)
"""
self.root_module_path = root_module_path
self.max_depth = max_depth
self.tree = {}

def _import_module(self, module_path: str) -> ModuleType | None:
"""Safely import a module.
Args:
module_path: String representing the module path (e.g., 'prefect.runtime')
"""
try:
return importlib.import_module(module_path)
except (ImportError, TypeError) as e:
print(f"Warning: Could not import {module_path}: {e}")
return None

def _is_defined_in_module(self, item: Any, current_module: str) -> bool:
"""Check if an item is defined in the current module.
Args:
item: The item to check
current_module: The current module path
"""
try:
if inspect.ismodule(item):
return False

# Get the module where this item is defined
if hasattr(item, "__module__"):
return item.__module__ == current_module

return False
except Exception:
return False

def _get_module_public_api(
self, module: ModuleType, module_path: str | None = None
) -> dict[str, list[str]]:
"""Get the public API of a module.
Args:
module: The module to get the public API of
module_path: The path of the module (e.g., 'prefect.runtime')
"""
api = {"all": [], "classes": [], "functions": [], "constants": []}

try:
if hasattr(module, "__all__"):
api["all"] = list(module.__all__)
items = {
name: getattr(module, name, None)
for name in module.__all__
if hasattr(module, name)
}
else:
# Get non-underscore attributes
items = {
name: getattr(module, name, None)
for name in dir(module)
if not name.startswith("_")
}

# Categorize items that we can safely inspect and are defined in our module
module_name = module_path or module.__name__
for name, item in items.items():
try:
if item is not None and self._is_defined_in_module(
item, module_name
):
if inspect.isclass(item):
api["classes"].append(name)
elif inspect.isfunction(item):
api["functions"].append(name)
elif not inspect.ismodule(item):
api["constants"].append(name)
except (TypeError, ValueError):
# Skip items we can't properly inspect
continue

except Exception as e:
print(f"Warning: Error inspecting module {module.__name__}: {e}")

return api

def _explore_submodules(
self, module: ModuleType, current_depth: int = 0
) -> dict[str, Any]:
"""Recursively explore submodules and their APIs.
Args:
module: The module to explore
current_depth: The current depth of the exploration
"""
result = {
"api": self._get_module_public_api(module, module.__name__),
"submodules": {},
}

if current_depth < self.max_depth:
try:
if hasattr(module, "__path__"):
for _, name, _ in pkgutil.iter_modules(module.__path__):
try:
full_name = f"{module.__name__}.{name}"
submodule = self._import_module(full_name)
if submodule:
result["submodules"][name] = self._explore_submodules(
submodule, current_depth + 1
)
except Exception as e:
print(f"Warning: Error exploring submodule {name}: {e}")
continue
except Exception as e:
print(
f"Warning: Error accessing module path for {module.__name__}: {e}"
)

return result

def explore(self) -> dict[str, Any]:
"""Explore the module tree starting from the root module.
Returns:
dict[str, Any]: The explored module tree
"""
root_module = self._import_module(self.root_module_path)
if root_module:
self.tree = self._explore_submodules(root_module)
return self.tree

def get_tree_string(
self, tree: dict[str, Any] | None = None, prefix: str = "", is_last: bool = True
) -> str:
"""Generate the module tree as a string in a hierarchical format.
Args:
tree: The module tree to generate a string for
prefix: The prefix to use for the tree
is_last: Whether the current module is the last in its parent
"""
lines = []
if tree is None:
tree = self.tree
lines.append(f"📦 {self.root_module_path}")

api = tree.get("api", {})
indent = " " if is_last else "│ "

# Add public API categories
if api.get("all"):
lines.append(
f"{prefix}{'└── ' if is_last else '├── '}📜 __all__: {', '.join(api['all'])}"
)

for category, items in api.items():
if category != "all" and items:
lines.append(
f"{prefix}{'└── ' if is_last and not tree['submodules'] else '├── '}"
f"{'🔷' if category == 'classes' else '⚡' if category == 'functions' else '📌'} "
f"{category}: {', '.join(sorted(items))}"
)

# Add submodules
submodules = tree.get("submodules", {})
for idx, (name, subtree) in enumerate(submodules.items()):
is_last_module = idx == len(submodules) - 1
lines.append(f"{prefix}{'└── ' if is_last_module else '├── '}📦 {name}")
lines.extend(
self.get_tree_string(
subtree, prefix + indent, is_last_module
).splitlines()
)

return "\n".join(lines)
Loading

0 comments on commit 4ec7b3f

Please sign in to comment.