Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: language server toolkit #165

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
793 changes: 793 additions & 0 deletions .goose-jedi-config.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ developer = "goose.toolkit.developer:Developer"
github = "goose.toolkit.github:Github"
jira = "goose.toolkit.jira:Jira"
screen = "goose.toolkit.screen:Screen"
language_server = "goose.toolkit.language_server:LanguageServerCoordinator"
reasoner = "goose.toolkit.reasoner:Reasoner"
repo_context = "goose.toolkit.repo_context.repo_context:RepoContext"

Expand All @@ -42,6 +43,9 @@ goose = "goose.cli.main:goose_cli"

[project.entry-points."goose.cli.group_option"]

[project.entry-points."goose.language_server"]
jedi = "goose.language_server.implementations.jedi:JediServer"

[project.scripts]
goose = "goose.cli.main:cli"

Expand All @@ -66,7 +70,8 @@ dev-dependencies = [
"mkdocstrings-python>=1.11.1",
"mkdocstrings>=0.26.1",
"pytest-mock>=3.14.0",
"pytest>=8.3.2"
"pytest>=8.3.2",
"pytest-asyncio>=0.24.0",
]

[tool.uv.sources]
Expand Down
2 changes: 1 addition & 1 deletion src/goose/cli/prompt/goose_prompt_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_message_after_commands(self, message: str) -> str:
command_value = ""

# execute the command with the given argument, expecting a return value
value_after_execution = self.commands[command_name].execute(command_value, message)
value_after_execution = self.commands[command_name].execute(command_value)

# if the command returns None, raise an error - this should never happen
# since the command should always return a string
Expand Down
59 changes: 36 additions & 23 deletions src/goose/cli/session.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from contextlib import nullcontext
import logging
import traceback
from pathlib import Path
Expand All @@ -18,6 +19,7 @@
from goose.cli.prompt.overwrite_session_prompt import OverwriteSessionPrompt
from goose.cli.session_notifier import SessionNotifier
from goose.profile import Profile
from goose.toolkit.language_server import LanguageServerCoordinator
from goose.utils import droid, load_plugins
from goose.utils._cost_calculator import get_total_cost_message
from goose.utils._create_exchange import create_exchange
Expand Down Expand Up @@ -76,6 +78,7 @@ def __init__(
self.prompt_session = GoosePromptSession()
self.status_indicator = Status("", spinner="dots")
self.notifier = SessionNotifier(self.status_indicator)
self.profile = load_profile(profile)
if not tracing:
logging.getLogger("langfuse").setLevel(logging.ERROR)
else:
Expand All @@ -89,7 +92,7 @@ def __init__(
)
langfuse_context.configure(enabled=tracing)

self.exchange = create_exchange(profile=load_profile(profile), notifier=self.notifier)
self.exchange = create_exchange(profile=self.profile, notifier=self.notifier)
setup_logging(log_file_directory=LOG_PATH, log_level=log_level)

self.exchange.messages.extend(self._get_initial_messages())
Expand Down Expand Up @@ -130,6 +133,12 @@ def setup_plan(self, plan: dict) -> None:
plan_tool_use = ToolUse(id="initialplan", name="update_plan", parameters=dict(tasks=tasks))
self.exchange.add_tool_use(plan_tool_use)

def setup_language_server(self) -> None:
if "language_server" not in [t.name for t in self.profile.toolkits]:
return nullcontext

return LanguageServerCoordinator.get_instance().language_server_client.start_servers

def process_first_message(self) -> Optional[Message]:
# Get a first input unless it has been specified, such as by a plan
if len(self.exchange.messages) == 0 or self.exchange.messages[-1].role == "assistant":
Expand Down Expand Up @@ -179,28 +188,32 @@ def run(self, new_session: bool = True) -> None:
print(f"[dim]starting session | name: [cyan]{self.name}[/cyan] profile: [cyan]{profile_name}[/cyan][/dim]")
print()
message = self.process_first_message()
while message: # Loop until no input (empty string).
self.notifier.start()
try:
self.exchange.add(message)
self.reply() # Process the user message.
except KeyboardInterrupt:
self.interrupt_reply()
except Exception:
# rewind to right before the last user message
self.exchange.rewind()
print(traceback.format_exc())
print(
"\n[red]The error above was an exception we were not able to handle.\n\n[/]"
+ "These errors are often related to connection or authentication\n"
+ "We've removed the conversation up to the most recent user message"
+ " - [yellow]depending on the error you may be able to continue[/]"
)
self.notifier.stop()
save_latest_session(self.session_file_path, self.exchange.messages)
print() # Print a newline for separation.
user_input = self.prompt_session.get_user_input()
message = Message.user(text=user_input.text) if user_input.to_continue() else None
with self.setup_language_server()() as _:
while message: # Loop until no input (empty string).
self.notifier.start()
try:
self.exchange.add(message)
self.reply() # Process the user message.
except KeyboardInterrupt:
self.interrupt_reply()
except Exception:
# rewind to right before the last user message
self.exchange.rewind()
print(traceback.format_exc())
print(
"\n[red]The error above was an exception we were not able to handle.\n\n[/]"
+ "These errors are often related to connection or authentication\n"
+ "We've removed the conversation up to the most recent user message"
+ " - [yellow]depending on the error you may be able to continue[/]"
)
self.notifier.stop()
save_latest_session(self.session_file_path, self.exchange.messages)

print() # Print a newline for separation.
# TODO: check time and notify on any diffed files that are in the opened file list.
# this is becuause the user may have edited files outside of the goose session.
user_input = self.prompt_session.get_user_input()
message = Message.user(text=user_input.text) if user_input.to_continue() else None

self._remove_empty_session()
self._log_cost()
Expand Down
Loading