diff options
author | Nate Sesti <sestinj@gmail.com> | 2023-05-30 08:41:56 -0400 |
---|---|---|
committer | Nate Sesti <sestinj@gmail.com> | 2023-05-30 08:41:56 -0400 |
commit | 9171567ebf2c0ddf7c430a160a50d0d47c0c991b (patch) | |
tree | 0a1f6c54ac719e472edc44c6643cd3ae258d0fb5 | |
parent | 5c049f2f3d797babb91c62ec752bf6a2c4c2f16a (diff) | |
download | sncontinue-9171567ebf2c0ddf7c430a160a50d0d47c0c991b.tar.gz sncontinue-9171567ebf2c0ddf7c430a160a50d0d47c0c991b.tar.bz2 sncontinue-9171567ebf2c0ddf7c430a160a50d0d47c0c991b.zip |
rename agent to autopilot
-rw-r--r-- | continuedev/src/continuedev/core/agent.py | 180 | ||||
-rw-r--r-- | continuedev/src/continuedev/libs/core.py | 22 | ||||
-rw-r--r-- | continuedev/src/continuedev/libs/util/copy_codebase.py | 16 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/ide.py | 16 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/notebook.py | 40 | ||||
-rw-r--r-- | docs/docs/concepts/agent.md | 7 | ||||
-rw-r--r-- | docs/docs/concepts/core.md | 5 | ||||
-rw-r--r-- | docs/docs/concepts/llm.md | 2 | ||||
-rw-r--r-- | docs/sidebars.js | 51 |
9 files changed, 260 insertions, 79 deletions
diff --git a/continuedev/src/continuedev/core/agent.py b/continuedev/src/continuedev/core/agent.py new file mode 100644 index 00000000..1996abb1 --- /dev/null +++ b/continuedev/src/continuedev/core/agent.py @@ -0,0 +1,180 @@ +import traceback +import time +from typing import Callable, Coroutine, List +from ..models.filesystem_edit import FileEditWithFullContents +from ..libs.llm import LLM +from .observation import Observation +from ..server.ide_protocol import AbstractIdeProtocolServer +from ..libs.util.queue import AsyncSubscriptionQueue +from ..models.main import ContinueBaseModel +from .main import Policy, History, FullState, Step, HistoryNode +from ..libs.steps.core.core import ReversibleStep, ManualEditStep, UserInputStep +from .sdk import ContinueSDK + + +class Autopilot(ContinueBaseModel): + llm: LLM + policy: Policy + ide: AbstractIdeProtocolServer + history: History = History.from_empty() + _on_update_callbacks: List[Callable[[FullState], None]] = [] + + _active: bool = False + _should_halt: bool = False + _main_user_input_queue: List[str] = [] + + _user_input_queue = AsyncSubscriptionQueue() + + class Config: + arbitrary_types_allowed = True + + def get_full_state(self) -> FullState: + return FullState(history=self.history, active=self._active, user_input_queue=self._main_user_input_queue) + + def on_update(self, callback: Callable[["FullState"], None]): + """Subscribe to changes to state""" + self._on_update_callbacks.append(callback) + + def update_subscribers(self): + full_state = self.get_full_state() + for callback in self._on_update_callbacks: + callback(full_state) + + def __get_step_params(self, step: "Step"): + return ContinueSDK(autopilot=self, llm=self.llm.with_system_message(step.system_message)) + + def give_user_input(self, input: str, index: int): + self._user_input_queue.post(index, input) + + async def wait_for_user_input(self) -> str: + self._active = False + self.update_subscribers() + user_input = await self._user_input_queue.get(self.history.current_index) + self._active = True + self.update_subscribers() + return user_input + + _manual_edits_buffer: List[FileEditWithFullContents] = [] + + async def reverse_to_index(self, index: int): + try: + while self.history.get_current_index() >= index: + current_step = self.history.get_current().step + self.history.step_back() + if issubclass(current_step.__class__, ReversibleStep): + await current_step.reverse(self.__get_step_params(current_step)) + + self.update_subscribers() + except Exception as e: + print(e) + + def handle_manual_edits(self, edits: List[FileEditWithFullContents]): + for edit in edits: + self._manual_edits_buffer.append(edit) + # TODO: You're storing a lot of unecessary data here. Can compress into EditDiffs on the spot, and merge. + # self._manual_edits_buffer = merge_file_edit(self._manual_edits_buffer, edit) + + def handle_traceback(self, traceback: str): + raise NotImplementedError + + _step_depth: int = 0 + + async def _run_singular_step(self, step: "Step", is_future_step: bool = False) -> Coroutine[Observation, None, None]: + if not is_future_step: + # Check manual edits buffer, clear out if needed by creating a ManualEditStep + if len(self._manual_edits_buffer) > 0: + manualEditsStep = ManualEditStep.from_sequence( + self._manual_edits_buffer) + self._manual_edits_buffer = [] + await self._run_singular_step(manualEditsStep) + + # Update history - do this first so we get top-first tree ordering + self.history.add_node(HistoryNode( + step=step, observation=None, depth=self._step_depth)) + + # Run step + self._step_depth += 1 + observation = await step(self.__get_step_params(step)) + self._step_depth -= 1 + + # Add observation to history + self.history.get_current().observation = observation + + # Update its description + step._set_description(await step.describe(self.llm)) + + # Call all subscribed callbacks + self.update_subscribers() + + return observation + + async def run_from_step(self, step: "Step"): + # if self._active: + # raise RuntimeError("Autopilot is already running") + self._active = True + + next_step = step + is_future_step = False + while not (next_step is None or self._should_halt): + try: + if is_future_step: + # If future step, then we are replaying and need to delete the step from history so it can be replaced + self.history.remove_current_and_substeps() + + observation = await self._run_singular_step(next_step, is_future_step) + if next_step := self.policy.next(self.history): + is_future_step = False + elif next_step := self.history.take_next_step(): + is_future_step = True + else: + next_step = None + + except Exception as e: + print( + f"Error while running step: \n{''.join(traceback.format_tb(e.__traceback__))}\n{e}") + next_step = None + + self._active = False + + # Doing this so active can make it to the frontend after steps are done. But want better state syncing tools + for callback in self._on_update_callbacks: + callback(None) + + async def run_from_observation(self, observation: Observation): + next_step = self.policy.next(self.history) + await self.run_from_step(next_step) + + async def run_policy(self): + first_step = self.policy.next(self.history) + await self.run_from_step(first_step) + + async def _request_halt(self): + if self._active: + self._should_halt = True + while self._active: + time.sleep(0.1) + self._should_halt = False + return None + + async def accept_user_input(self, user_input: str): + self._main_user_input_queue.append(user_input) + self.update_subscribers() + + if len(self._main_user_input_queue) > 1: + return + + # await self._request_halt() + # Just run the step that takes user input, and + # then up to the policy to decide how to deal with it. + self._main_user_input_queue.pop(0) + self.update_subscribers() + await self.run_from_step(UserInputStep(user_input=user_input)) + + while len(self._main_user_input_queue) > 0: + await self.run_from_step(UserInputStep( + user_input=self._main_user_input_queue.pop(0))) + + async def accept_refinement_input(self, user_input: str, index: int): + await self._request_halt() + await self.reverse_to_index(index) + await self.run_from_step(UserInputStep(user_input=user_input)) diff --git a/continuedev/src/continuedev/libs/core.py b/continuedev/src/continuedev/libs/core.py index 6a8a83ba..36d94ad1 100644 --- a/continuedev/src/continuedev/libs/core.py +++ b/continuedev/src/continuedev/libs/core.py @@ -89,31 +89,31 @@ class ContinueSDK: """The SDK provided as parameters to a step""" llm: LLM ide: AbstractIdeProtocolServer - __agent: "Agent" + __autopilot: "Autopilot" - def __init__(self, agent: "Agent", llm: Union[LLM, None] = None): + def __init__(self, autopilot: "Autopilot", llm: Union[LLM, None] = None): if llm is None: - self.llm = agent.llm + self.llm = autopilot.llm else: self.llm = llm - self.ide = agent.ide - self.__agent = agent + self.ide = autopilot.ide + self.__autopilot = autopilot @property def history(self) -> History: - return self.__agent.history + return self.__autopilot.history async def run_step(self, step: "Step") -> Coroutine[Observation, None, None]: - return await self.__agent._run_singular_step(step) + return await self.__autopilot._run_singular_step(step) async def apply_filesystem_edit(self, edit: FileSystemEdit): await self.run_step(FileSystemEditStep(edit=edit)) async def wait_for_user_input(self) -> str: - return await self.__agent.wait_for_user_input() + return await self.__autopilot.wait_for_user_input() -class Agent(ContinueBaseModel): +class Autopilot(ContinueBaseModel): llm: LLM policy: Policy ide: AbstractIdeProtocolServer @@ -142,7 +142,7 @@ class Agent(ContinueBaseModel): callback(full_state) def __get_step_params(self, step: "Step"): - return ContinueSDK(agent=self, llm=self.llm.with_system_message(step.system_message)) + return ContinueSDK(autopilot=self, llm=self.llm.with_system_message(step.system_message)) def give_user_input(self, input: str, index: int): self._user_input_queue.post(index, input) @@ -210,7 +210,7 @@ class Agent(ContinueBaseModel): async def run_from_step(self, step: "Step"): # if self._active: - # raise RuntimeError("Agent is already running") + # raise RuntimeError("Autopilot is already running") self._active = True next_step = step diff --git a/continuedev/src/continuedev/libs/util/copy_codebase.py b/continuedev/src/continuedev/libs/util/copy_codebase.py index ef1db72b..af957a34 100644 --- a/continuedev/src/continuedev/libs/util/copy_codebase.py +++ b/continuedev/src/continuedev/libs/util/copy_codebase.py @@ -5,7 +5,7 @@ from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler from ..models.main import FileEdit, DeleteDirectory, DeleteFile, AddDirectory, AddFile, FileSystemEdit, Position, Range, RenameFile, RenameDirectory, SequentialFileSystemEdit from ..models.filesystem import FileSystem -from ..libs.main import Agent +from ..libs.main import Autopilot from ..libs.map_path import map_path from ..libs.steps.main import ManualEditAction import shutil @@ -65,15 +65,15 @@ def calculate_diff(filepath: str, original: str, updated: str) -> List[FileEdit] # The whole usage of watchdog here should only be specific to RealFileSystem, you want to have a different "Observer" class for VirtualFileSystem, which would depend on being sent notifications class CopyCodebaseEventHandler(PatternMatchingEventHandler): - def __init__(self, ignore_directories: List[str], ignore_patterns: List[str], agent: Agent, orig_root: str, copy_root: str, filesystem: FileSystem): + def __init__(self, ignore_directories: List[str], ignore_patterns: List[str], autopilot: Autopilot, orig_root: str, copy_root: str, filesystem: FileSystem): super().__init__(ignore_directories=ignore_directories, ignore_patterns=ignore_patterns) - self.agent = agent + self.autopilot = autopilot self.orig_root = orig_root self.copy_root = copy_root self.filesystem = filesystem - # For now, we'll just make the update immediately, but eventually need to sync with agent. - # It should be the agent that makes the update right? It's just another action, everything comes from a single stream. + # For now, we'll just make the update immediately, but eventually need to sync with autopilot. + # It should be the autopilot that makes the update right? It's just another action, everything comes from a single stream. def _event_to_edit(self, event) -> Union[FileSystemEdit, None]: # NOTE: You'll need to map paths to create both an action within the copy filesystem (the one you take) and one in the original fileystem (the one you'll record and allow the user to accept). Basically just need a converter built in to the FileSystemEdit class @@ -110,13 +110,13 @@ class CopyCodebaseEventHandler(PatternMatchingEventHandler): return edit = edit.with_mapped_paths(self.orig_root, self.copy_root) action = ManualEditAction(edit) - self.agent.act(action) + self.autopilot.act(action) -def maintain_copy_workspace(agent: Agent, filesystem: FileSystem, orig_root: str, copy_root: str): +def maintain_copy_workspace(autopilot: Autopilot, filesystem: FileSystem, orig_root: str, copy_root: str): observer = Observer() event_handler = CopyCodebaseEventHandler( - [".git"], [], agent, orig_root, copy_root, filesystem) + [".git"], [], autopilot, orig_root, copy_root, filesystem) observer.schedule(event_handler, orig_root, recursive=True) observer.start() try: diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index dd1dc463..167d9483 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -122,7 +122,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): pass async def setFileOpen(self, filepath: str, open: bool = True): - # Agent needs access to this. + # Autopilot needs access to this. await self.websocket.send_json({ "messageType": "setFileOpen", "filepath": filepath, @@ -147,12 +147,12 @@ class IdeProtocolServer(AbstractIdeProtocolServer): responses = await asyncio.gather(*[ self._receive_json(ShowSuggestionResponse) for i in range(len(suggestions)) - ]) # WORKING ON THIS FLOW HERE. Fine now to just await for response, instead of doing something fancy with a "waiting" state on the agent. + ]) # WORKING ON THIS FLOW HERE. Fine now to just await for response, instead of doing something fancy with a "waiting" state on the autopilot. # Just need connect the suggestionId to the IDE (and the notebook) return any([r.accepted for r in responses]) # ------------------------------- # - # Here needs to pass message onto the Agent OR Agent just subscribes. + # Here needs to pass message onto the Autopilot OR Autopilot just subscribes. # This is where you might have triggers: plugins can subscribe to certian events # like file changes, tracebacks, etc... @@ -160,12 +160,12 @@ class IdeProtocolServer(AbstractIdeProtocolServer): pass def onTraceback(self, traceback: Traceback): - # Same as below, maybe not every agent? + # Same as below, maybe not every autopilot? for _, session in self.session_manager.sessions.items(): - session.agent.handle_traceback(traceback) + session.autopilot.handle_traceback(traceback) def onFileSystemUpdate(self, update: FileSystemEdit): - # Access to Agent (so SessionManager) + # Access to Autopilot (so SessionManager) pass def onCloseNotebook(self, session_id: str): @@ -176,10 +176,10 @@ class IdeProtocolServer(AbstractIdeProtocolServer): pass def onFileEdits(self, edits: List[FileEditWithFullContents]): - # Send the file edits to ALL agents. + # Send the file edits to ALL autopilots. # Maybe not ideal behavior for _, session in self.session_manager.sessions.items(): - session.agent.handle_manual_edits(edits) + session.autopilot.handle_manual_edits(edits) # Request information. Session doesn't matter. async def getOpenFiles(self) -> List[str]: diff --git a/continuedev/src/continuedev/server/notebook.py b/continuedev/src/continuedev/server/notebook.py index c9d4edc5..c26920f5 100644 --- a/continuedev/src/continuedev/server/notebook.py +++ b/continuedev/src/continuedev/server/notebook.py @@ -6,7 +6,7 @@ from uvicorn.main import Server from ..models.filesystem_edit import FileEditWithFullContents from ..libs.policy import DemoPolicy -from ..libs.core import Agent, FullState, History, Step +from ..libs.core import Autopilot, FullState, History, Step from ..libs.steps.nate import ImplementAbstractMethodStep from ..libs.observation import Observation from dotenv import load_dotenv @@ -41,16 +41,16 @@ Server.handle_exit = AppStatus.handle_exit class Session: session_id: str - agent: Agent + autopilot: Autopilot ws: Union[WebSocket, None] - def __init__(self, session_id: str, agent: Agent): + def __init__(self, session_id: str, autopilot: Autopilot): self.session_id = session_id - self.agent = agent + self.autopilot = autopilot self.ws = None -class DemoAgent(Agent): +class DemoAutopilot(Autopilot): first_seen: bool = False cumulative_edit_string = "" @@ -78,20 +78,20 @@ class SessionManager: def new_session(self, ide: AbstractIdeProtocolServer) -> str: cmd = "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py" - agent = DemoAgent(llm=OpenAI(api_key=openai_api_key), - policy=DemoPolicy(cmd=cmd), ide=ide) + autopilot = DemoAutopilot(llm=OpenAI(api_key=openai_api_key), + policy=DemoPolicy(cmd=cmd), ide=ide) session_id = str(uuid4()) - session = Session(session_id=session_id, agent=agent) + session = Session(session_id=session_id, autopilot=autopilot) self.sessions[session_id] = session def on_update(state: FullState): session_manager.send_ws_data(session_id, { "messageType": "state", - "state": agent.get_full_state().dict() + "state": autopilot.get_full_state().dict() }) - agent.on_update(on_update) - asyncio.create_task(agent.run_policy()) + autopilot.on_update(on_update) + asyncio.create_task(autopilot.run_policy()) return session_id def remove_session(self, session_id: str): @@ -147,7 +147,7 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we # Update any history that may have happened before connection await websocket.send_json({ "messageType": "state", - "state": session_manager.get_session(session.session_id).agent.get_full_state().dict() + "state": session_manager.get_session(session.session_id).autopilot.get_full_state().dict() }) print("Session started", data) while AppStatus.should_exit is False: @@ -162,17 +162,17 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we if messageType == "main_input": # Do something with user input asyncio.create_task( - session.agent.accept_user_input(data["value"])) + session.autopilot.accept_user_input(data["value"])) elif messageType == "step_user_input": asyncio.create_task( - session.agent.give_user_input(data["value"], data["index"])) + session.autopilot.give_user_input(data["value"], data["index"])) elif messageType == "refinement_input": asyncio.create_task( - session.agent.accept_refinement_input(data["value"], data["index"])) + session.autopilot.accept_refinement_input(data["value"], data["index"])) elif messageType == "reverse": # Reverse the history to the given index asyncio.create_task( - session.agent.reverse_to_index(data["index"])) + session.autopilot.reverse_to_index(data["index"])) except Exception as e: print(e) @@ -182,17 +182,17 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we @router.post("/run") def request_run(step: Step, session=Depends(session)): - """Tell an agent to take a specific action.""" - asyncio.create_task(session.agent.run_from_step(step)) + """Tell an autopilot to take a specific action.""" + asyncio.create_task(session.autopilot.run_from_step(step)) return "Success" @router.get("/history") def get_history(session=Depends(session)) -> History: - return session.agent.history + return session.autopilot.history @router.post("/observation") def post_observation(observation: Observation, session=Depends(session)): - asyncio.create_task(session.agent.run_from_observation(observation)) + asyncio.create_task(session.autopilot.run_from_observation(observation)) return "Success" diff --git a/docs/docs/concepts/agent.md b/docs/docs/concepts/agent.md index 0528b305..e2fa6832 100644 --- a/docs/docs/concepts/agent.md +++ b/docs/docs/concepts/agent.md @@ -1,9 +1,10 @@ -# Agent
+# Autopilot
+
+`Autopilot` contains the
-`Agent` contains the
- History
- LLM
- Policy
- IDE
-**Q: should we really call this abstraction agent?**
\ No newline at end of file +**Q: should we really call this abstraction autopilot?**
diff --git a/docs/docs/concepts/core.md b/docs/docs/concepts/core.md index ee58cbb2..d60f46ac 100644 --- a/docs/docs/concepts/core.md +++ b/docs/docs/concepts/core.md @@ -3,11 +3,12 @@ The `Core` connects the SDK and GUI with the IDE (i.e. in VS Code, a web browser, etc), enabling the steps to make changes to your code and accelerate your software development workflows.
The `Core` includes
+
- IDE protocol
- GUI protocol
- SDK
-- Agent
+- Autopilot
There is a two-way sync between an IDE and the GUI that happens through Core.
-**Q: does this make sense as a concept?**
\ No newline at end of file +**Q: does this make sense as a concept?**
diff --git a/docs/docs/concepts/llm.md b/docs/docs/concepts/llm.md index 11bbacc7..8c2dbcba 100644 --- a/docs/docs/concepts/llm.md +++ b/docs/docs/concepts/llm.md @@ -4,4 +4,4 @@ **Q: should we call this LLM? Perhaps just model?**
-**Q: should this abstraction be connected to agent?**
\ No newline at end of file +**Q: should this abstraction be connected to autopilot?**
diff --git a/docs/sidebars.js b/docs/sidebars.js index befe4feb..d2c91388 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -13,41 +13,40 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - docsSidebar: [ - 'intro', - 'getting-started', - 'install', - 'how-continue-works', + "intro", + "getting-started", + "install", + "how-continue-works", { - type: 'category', - label: 'Walkthroughs', + type: "category", + label: "Walkthroughs", items: [ - 'walkthroughs/use-the-gui', - 'walkthroughs/use-a-recipe', - 'walkthroughs/create-a-recipe', - 'walkthroughs/share-a-recipe', + "walkthroughs/use-the-gui", + "walkthroughs/use-a-recipe", + "walkthroughs/create-a-recipe", + "walkthroughs/share-a-recipe", ], }, { - type: 'category', - label: 'Concepts', + type: "category", + label: "Concepts", items: [ - 'concepts/agent', - 'concepts/core', - 'concepts/gui', - 'concepts/history', - 'concepts/ide', - 'concepts/llm', - 'concepts/policy', - 'concepts/recipe', - 'concepts/sdk', - 'concepts/step', + "concepts/autopilot", + "concepts/core", + "concepts/gui", + "concepts/history", + "concepts/ide", + "concepts/llm", + "concepts/policy", + "concepts/recipe", + "concepts/sdk", + "concepts/step", ], }, - 'sdk', - 'telemetry', + "sdk", + "telemetry", ], }; -module.exports = sidebars;
\ No newline at end of file +module.exports = sidebars; |