From f7a72cbb9c5993788378c6dd1363116b8e145ad1 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sun, 23 Jul 2023 13:20:33 -0700 Subject: refactoring --- continuedev/src/continuedev/core/autopilot.py | 2 +- continuedev/src/continuedev/core/policy.py | 10 +- continuedev/src/continuedev/core/sdk.py | 4 +- .../continuedev/libs/util/step_name_to_steps.py | 26 +- .../plugins/recipes/AddTransformRecipe/README.md | 8 + .../AddTransformRecipe/dlt_transform_docs.md | 135 ++++ .../plugins/recipes/AddTransformRecipe/main.py | 27 + .../plugins/recipes/AddTransformRecipe/steps.py | 82 +++ .../plugins/recipes/ContinueRecipeRecipe/README.md | 7 + .../plugins/recipes/ContinueRecipeRecipe/main.py | 36 + .../plugins/recipes/CreatePipelineRecipe/README.md | 0 .../plugins/recipes/CreatePipelineRecipe/main.py | 33 + .../plugins/recipes/CreatePipelineRecipe/steps.py | 168 +++++ .../plugins/recipes/DDtoBQRecipe/README.md | 3 + .../DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md | 85 +++ .../plugins/recipes/DDtoBQRecipe/main.py | 26 + .../plugins/recipes/DDtoBQRecipe/steps.py | 94 +++ .../recipes/DeployPipelineAirflowRecipe/README.md | 0 .../recipes/DeployPipelineAirflowRecipe/main.py | 61 ++ .../recipes/DeployPipelineAirflowRecipe/steps.py | 88 +++ .../src/continuedev/plugins/recipes/README.md | 19 + .../plugins/recipes/TemplateRecipe/README.md | 7 + .../plugins/recipes/TemplateRecipe/main.py | 29 + .../plugins/recipes/WritePytestsRecipe/README.md | 7 + .../plugins/recipes/WritePytestsRecipe/main.py | 49 ++ .../src/continuedev/plugins/steps/README.md | 50 ++ .../src/continuedev/plugins/steps/__init__.py | 1 + continuedev/src/continuedev/plugins/steps/chat.py | 270 ++++++++ .../src/continuedev/plugins/steps/chroma.py | 80 +++ .../src/continuedev/plugins/steps/clear_history.py | 10 + .../src/continuedev/plugins/steps/comment_code.py | 12 + .../src/continuedev/plugins/steps/core/core.py | 731 +++++++++++++++++++++ .../continuedev/plugins/steps/custom_command.py | 28 + .../plugins/steps/draft/abstract_method.py | 19 + .../continuedev/plugins/steps/draft/migration.py | 30 + .../src/continuedev/plugins/steps/draft/redux.py | 47 ++ .../src/continuedev/plugins/steps/draft/typeorm.py | 43 ++ .../src/continuedev/plugins/steps/feedback.py | 17 + .../continuedev/plugins/steps/find_and_replace.py | 28 + continuedev/src/continuedev/plugins/steps/help.py | 59 ++ .../plugins/steps/input/nl_multiselect.py | 28 + continuedev/src/continuedev/plugins/steps/main.py | 314 +++++++++ .../src/continuedev/plugins/steps/on_traceback.py | 27 + .../src/continuedev/plugins/steps/open_config.py | 29 + continuedev/src/continuedev/plugins/steps/react.py | 42 ++ .../continuedev/plugins/steps/search_directory.py | 69 ++ .../continuedev/plugins/steps/steps_on_startup.py | 17 + .../src/continuedev/plugins/steps/welcome.py | 33 + .../recipes/AddTransformRecipe/README.md | 8 - .../AddTransformRecipe/dlt_transform_docs.md | 135 ---- .../continuedev/recipes/AddTransformRecipe/main.py | 27 - .../recipes/AddTransformRecipe/steps.py | 86 --- .../recipes/ContinueRecipeRecipe/README.md | 7 - .../recipes/ContinueRecipeRecipe/main.py | 37 -- .../recipes/CreatePipelineRecipe/README.md | 0 .../recipes/CreatePipelineRecipe/main.py | 33 - .../recipes/CreatePipelineRecipe/steps.py | 170 ----- .../src/continuedev/recipes/DDtoBQRecipe/README.md | 3 - .../DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md | 85 --- .../src/continuedev/recipes/DDtoBQRecipe/main.py | 27 - .../src/continuedev/recipes/DDtoBQRecipe/steps.py | 100 --- .../recipes/DeployPipelineAirflowRecipe/README.md | 0 .../recipes/DeployPipelineAirflowRecipe/main.py | 62 -- .../recipes/DeployPipelineAirflowRecipe/steps.py | 97 --- continuedev/src/continuedev/recipes/README.md | 17 - .../continuedev/recipes/TemplateRecipe/README.md | 7 - .../src/continuedev/recipes/TemplateRecipe/main.py | 27 - .../recipes/WritePytestsRecipe/README.md | 7 - .../continuedev/recipes/WritePytestsRecipe/main.py | 48 -- continuedev/src/continuedev/steps/README.md | 50 -- continuedev/src/continuedev/steps/__init__.py | 1 - continuedev/src/continuedev/steps/chat.py | 270 -------- continuedev/src/continuedev/steps/chroma.py | 80 --- continuedev/src/continuedev/steps/clear_history.py | 10 - continuedev/src/continuedev/steps/comment_code.py | 12 - continuedev/src/continuedev/steps/core/core.py | 731 --------------------- .../src/continuedev/steps/custom_command.py | 28 - .../src/continuedev/steps/draft/abstract_method.py | 19 - .../src/continuedev/steps/draft/migration.py | 30 - continuedev/src/continuedev/steps/draft/redux.py | 47 -- continuedev/src/continuedev/steps/draft/typeorm.py | 43 -- continuedev/src/continuedev/steps/feedback.py | 17 - .../src/continuedev/steps/find_and_replace.py | 28 - continuedev/src/continuedev/steps/help.py | 59 -- .../src/continuedev/steps/input/nl_multiselect.py | 28 - continuedev/src/continuedev/steps/main.py | 318 --------- continuedev/src/continuedev/steps/on_traceback.py | 27 - continuedev/src/continuedev/steps/open_config.py | 29 - continuedev/src/continuedev/steps/react.py | 43 -- .../src/continuedev/steps/search_directory.py | 69 -- .../src/continuedev/steps/steps_on_startup.py | 23 - continuedev/src/continuedev/steps/welcome.py | 32 - 92 files changed, 2969 insertions(+), 2998 deletions(-) create mode 100644 continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/dlt_transform_docs.md create mode 100644 continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/steps.py create mode 100644 continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py create mode 100644 continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md create mode 100644 continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/steps.py create mode 100644 continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/steps.py create mode 100644 continuedev/src/continuedev/plugins/recipes/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/TemplateRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/TemplateRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/README.md create mode 100644 continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py create mode 100644 continuedev/src/continuedev/plugins/steps/README.md create mode 100644 continuedev/src/continuedev/plugins/steps/__init__.py create mode 100644 continuedev/src/continuedev/plugins/steps/chat.py create mode 100644 continuedev/src/continuedev/plugins/steps/chroma.py create mode 100644 continuedev/src/continuedev/plugins/steps/clear_history.py create mode 100644 continuedev/src/continuedev/plugins/steps/comment_code.py create mode 100644 continuedev/src/continuedev/plugins/steps/core/core.py create mode 100644 continuedev/src/continuedev/plugins/steps/custom_command.py create mode 100644 continuedev/src/continuedev/plugins/steps/draft/abstract_method.py create mode 100644 continuedev/src/continuedev/plugins/steps/draft/migration.py create mode 100644 continuedev/src/continuedev/plugins/steps/draft/redux.py create mode 100644 continuedev/src/continuedev/plugins/steps/draft/typeorm.py create mode 100644 continuedev/src/continuedev/plugins/steps/feedback.py create mode 100644 continuedev/src/continuedev/plugins/steps/find_and_replace.py create mode 100644 continuedev/src/continuedev/plugins/steps/help.py create mode 100644 continuedev/src/continuedev/plugins/steps/input/nl_multiselect.py create mode 100644 continuedev/src/continuedev/plugins/steps/main.py create mode 100644 continuedev/src/continuedev/plugins/steps/on_traceback.py create mode 100644 continuedev/src/continuedev/plugins/steps/open_config.py create mode 100644 continuedev/src/continuedev/plugins/steps/react.py create mode 100644 continuedev/src/continuedev/plugins/steps/search_directory.py create mode 100644 continuedev/src/continuedev/plugins/steps/steps_on_startup.py create mode 100644 continuedev/src/continuedev/plugins/steps/welcome.py delete mode 100644 continuedev/src/continuedev/recipes/AddTransformRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/AddTransformRecipe/dlt_transform_docs.md delete mode 100644 continuedev/src/continuedev/recipes/AddTransformRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/AddTransformRecipe/steps.py delete mode 100644 continuedev/src/continuedev/recipes/ContinueRecipeRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/ContinueRecipeRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/CreatePipelineRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py delete mode 100644 continuedev/src/continuedev/recipes/DDtoBQRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md delete mode 100644 continuedev/src/continuedev/recipes/DDtoBQRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/DDtoBQRecipe/steps.py delete mode 100644 continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/steps.py delete mode 100644 continuedev/src/continuedev/recipes/README.md delete mode 100644 continuedev/src/continuedev/recipes/TemplateRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/TemplateRecipe/main.py delete mode 100644 continuedev/src/continuedev/recipes/WritePytestsRecipe/README.md delete mode 100644 continuedev/src/continuedev/recipes/WritePytestsRecipe/main.py delete mode 100644 continuedev/src/continuedev/steps/README.md delete mode 100644 continuedev/src/continuedev/steps/__init__.py delete mode 100644 continuedev/src/continuedev/steps/chat.py delete mode 100644 continuedev/src/continuedev/steps/chroma.py delete mode 100644 continuedev/src/continuedev/steps/clear_history.py delete mode 100644 continuedev/src/continuedev/steps/comment_code.py delete mode 100644 continuedev/src/continuedev/steps/core/core.py delete mode 100644 continuedev/src/continuedev/steps/custom_command.py delete mode 100644 continuedev/src/continuedev/steps/draft/abstract_method.py delete mode 100644 continuedev/src/continuedev/steps/draft/migration.py delete mode 100644 continuedev/src/continuedev/steps/draft/redux.py delete mode 100644 continuedev/src/continuedev/steps/draft/typeorm.py delete mode 100644 continuedev/src/continuedev/steps/feedback.py delete mode 100644 continuedev/src/continuedev/steps/find_and_replace.py delete mode 100644 continuedev/src/continuedev/steps/help.py delete mode 100644 continuedev/src/continuedev/steps/input/nl_multiselect.py delete mode 100644 continuedev/src/continuedev/steps/main.py delete mode 100644 continuedev/src/continuedev/steps/on_traceback.py delete mode 100644 continuedev/src/continuedev/steps/open_config.py delete mode 100644 continuedev/src/continuedev/steps/react.py delete mode 100644 continuedev/src/continuedev/steps/search_directory.py delete mode 100644 continuedev/src/continuedev/steps/steps_on_startup.py delete mode 100644 continuedev/src/continuedev/steps/welcome.py (limited to 'continuedev') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index afbfc7ed..ecc587ce 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -13,7 +13,7 @@ from ..server.ide_protocol import AbstractIdeProtocolServer from ..libs.util.queue import AsyncSubscriptionQueue from ..models.main import ContinueBaseModel from .main import Context, ContinueCustomException, HighlightedRangeContext, Policy, History, FullState, Step, HistoryNode -from ..steps.core.core import ReversibleStep, ManualEditStep, UserInputStep +from ..plugins.steps.core.core import ReversibleStep, ManualEditStep, UserInputStep from ..libs.util.telemetry import capture_event from .sdk import ContinueSDK from ..libs.util.step_name_to_steps import get_step_from_name diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py index 05f03bdc..dfa0e7f9 100644 --- a/continuedev/src/continuedev/core/policy.py +++ b/continuedev/src/continuedev/core/policy.py @@ -1,15 +1,15 @@ from textwrap import dedent from typing import Union -from ..steps.chat import SimpleChatStep -from ..steps.welcome import WelcomeStep +from ..plugins.steps.chat import SimpleChatStep +from ..plugins.steps.welcome import WelcomeStep from .config import ContinueConfig -from ..steps.steps_on_startup import StepsOnStartupStep +from ..plugins.steps.steps_on_startup import StepsOnStartupStep from .main import Step, History, Policy from .observation import UserInputObservation -from ..steps.core.core import MessageStep +from ..plugins.steps.core.core import MessageStep from ..libs.util.step_name_to_steps import get_step_from_name -from ..steps.custom_command import CustomCommandStep +from ..plugins.steps.custom_command import CustomCommandStep def parse_slash_command(inp: str, config: ContinueConfig) -> Union[None, Step]: diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py index 37a51efa..9d1025e3 100644 --- a/continuedev/src/continuedev/core/sdk.py +++ b/continuedev/src/continuedev/core/sdk.py @@ -3,7 +3,7 @@ from functools import cached_property from typing import Coroutine, Dict, Union import os -from ..steps.core.core import DefaultModelEditCodeStep +from ..plugins.steps.core.core import DefaultModelEditCodeStep from ..models.main import Range from .abstract_sdk import AbstractContinueSDK from .config import ContinueConfig, load_config, load_global_config, update_global_config @@ -16,7 +16,7 @@ from ..libs.llm.ggml import GGML from .observation import Observation from ..server.ide_protocol import AbstractIdeProtocolServer from .main import Context, ContinueCustomException, History, HistoryNode, Step, ChatMessage -from ..steps.core.core import * +from ..plugins.steps.core.core import * from ..libs.llm.proxy_server import ProxyServer diff --git a/continuedev/src/continuedev/libs/util/step_name_to_steps.py b/continuedev/src/continuedev/libs/util/step_name_to_steps.py index 49056c81..baa25da6 100644 --- a/continuedev/src/continuedev/libs/util/step_name_to_steps.py +++ b/continuedev/src/continuedev/libs/util/step_name_to_steps.py @@ -1,19 +1,19 @@ from typing import Dict from ...core.main import Step -from ...steps.core.core import UserInputStep -from ...steps.main import EditHighlightedCodeStep -from ...steps.chat import SimpleChatStep -from ...steps.comment_code import CommentCodeStep -from ...steps.feedback import FeedbackStep -from ...recipes.AddTransformRecipe.main import AddTransformRecipe -from ...recipes.CreatePipelineRecipe.main import CreatePipelineRecipe -from ...recipes.DDtoBQRecipe.main import DDtoBQRecipe -from ...recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe -from ...steps.on_traceback import DefaultOnTracebackStep -from ...steps.clear_history import ClearHistoryStep -from ...steps.open_config import OpenConfigStep -from ...steps.help import HelpStep +from ...plugins.steps.core.core import UserInputStep +from ...plugins.steps.main import EditHighlightedCodeStep +from ...plugins.steps.chat import SimpleChatStep +from ...plugins.steps.comment_code import CommentCodeStep +from ...plugins.steps.feedback import FeedbackStep +from ...plugins.recipes.AddTransformRecipe.main import AddTransformRecipe +from ...plugins.recipes.CreatePipelineRecipe.main import CreatePipelineRecipe +from ...plugins.recipes.DDtoBQRecipe.main import DDtoBQRecipe +from ...plugins.recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe +from ...plugins.steps.on_traceback import DefaultOnTracebackStep +from ...plugins.steps.clear_history import ClearHistoryStep +from ...plugins.steps.open_config import OpenConfigStep +from ...plugins.steps.help import HelpStep # This mapping is used to convert from string in ContinueConfig json to corresponding Step class. # Used for example in slash_commands and steps_on_startup diff --git a/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/README.md new file mode 100644 index 00000000..d735e0cd --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/README.md @@ -0,0 +1,8 @@ +# AddTransformRecipe + +Uses the Chess.com API example to show how to add map and filter Python transforms to a dlt pipeline. + +Background +- https://dlthub.com/docs/general-usage/resource#filter-transform-and-pivot-data +- https://dlthub.com/docs/customizations/customizing-pipelines/renaming_columns +- https://dlthub.com/docs/customizations/customizing-pipelines/pseudonymizing_columns \ No newline at end of file diff --git a/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/dlt_transform_docs.md b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/dlt_transform_docs.md new file mode 100644 index 00000000..658b285f --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/dlt_transform_docs.md @@ -0,0 +1,135 @@ +# Customize resources +## Filter, transform and pivot data + +You can attach any number of transformations that are evaluated on item per item basis to your resource. The available transformation types: +- map - transform the data item (resource.add_map) +- filter - filter the data item (resource.add_filter) +- yield map - a map that returns iterator (so single row may generate many rows - resource.add_yield_map) + +Example: We have a resource that loads a list of users from an api endpoint. We want to customize it so: +- we remove users with user_id == 'me' +- we anonymize user data +Here's our resource: +```python +import dlt + +@dlt.resource(write_disposition='replace') +def users(): + ... + users = requests.get(...) + ... + yield users +``` + +Here's our script that defines transformations and loads the data. +```python +from pipedrive import users + +def anonymize_user(user_data): + user_data['user_id'] = hash_str(user_data['user_id']) + user_data['user_email'] = hash_str(user_data['user_email']) + return user_data + +# add the filter and anonymize function to users resource and enumerate +for user in users().add_filter(lambda user: user['user_id'] != 'me').add_map(anonymize_user): +print(user) +``` + +Here is a more complex example of a filter transformation: + + # Renaming columns + ## Renaming columns by replacing the special characters + + In the example below, we create a dummy source with special characters in the name. We then write a function that we intend to apply to the resource to modify its output (i.e. replacing the German umlaut): replace_umlauts_in_dict_keys. + ```python + import dlt + + # create a dummy source with umlauts (special characters) in key names (um) + @dlt.source + def dummy_source(prefix: str = None): + @dlt.resource + def dummy_data(): + for _ in range(100): + yield {f'Objekt_{_}':{'Größe':_, 'Äquivalenzprüfung':True}} + return dummy_data(), + + def replace_umlauts_in_dict_keys(d): + # Replaces umlauts in dictionary keys with standard characters. + umlaut_map = {'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', 'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue'} + result = {} + for k, v in d.items(): + new_key = ''.join(umlaut_map.get(c, c) for c in k) + if isinstance(v, dict): + result[new_key] = replace_umlauts_in_dict_keys(v) + else: + result[new_key] = v + return result + + # We can add the map function to the resource + + # 1. Create an instance of the source so you can edit it. + data_source = dummy_source() + + # 2. Modify this source instance's resource + data_source = data_source.dummy_data().add_map(replace_umlauts_in_dict_keys) + + # 3. Inspect your result + for row in data_source: + print(row) + + # {'Objekt_0': {'Groesse': 0, 'Aequivalenzpruefung': True}} + # ... + ``` + +Here is a more complex example of a map transformation: + +# Pseudonymizing columns +## Pseudonymizing (or anonymizing) columns by replacing the special characters +Pseudonymization is a deterministic way to hide personally identifiable info (PII), enabling us to consistently achieve the same mapping. If instead you wish to anonymize, you can delete the data, or replace it with a constant. In the example below, we create a dummy source with a PII column called 'name', which we replace with deterministic hashes (i.e. replacing the German umlaut). + +```python +import dlt +import hashlib + +@dlt.source +def dummy_source(prefix: str = None): + @dlt.resource + def dummy_data(): + for _ in range(3): + yield {'id':_, 'name': f'Jane Washington {_}'} + return dummy_data(), + +def pseudonymize_name(doc): + Pseudonmyisation is a deterministic type of PII-obscuring + Its role is to allow identifying users by their hash, without revealing the underlying info. + + # add a constant salt to generate + salt = 'WI@N57%zZrmk#88c' + salted_string = doc['name'] + salt + sh = hashlib.sha256() + sh.update(salted_string.encode()) + hashed_string = sh.digest().hex() + doc['name'] = hashed_string + return doc + + # run it as is + for row in dummy_source().dummy_data().add_map(pseudonymize_name): + print(row) + + #{'id': 0, 'name': '96259edb2b28b48bebce8278c550e99fbdc4a3fac8189e6b90f183ecff01c442'} + #{'id': 1, 'name': '92d3972b625cbd21f28782fb5c89552ce1aa09281892a2ab32aee8feeb3544a1'} + #{'id': 2, 'name': '443679926a7cff506a3b5d5d094dc7734861352b9e0791af5d39db5a7356d11a'} + + # Or create an instance of the data source, modify the resource and run the source. + + # 1. Create an instance of the source so you can edit it. + data_source = dummy_source() + # 2. Modify this source instance's resource + data_source = data_source.dummy_data().add_map(replace_umlauts_in_dict_keys) + # 3. Inspect your result + for row in data_source: + print(row) + + pipeline = dlt.pipeline(pipeline_name='example', destination='bigquery', dataset_name='normalized_data') + load_info = pipeline.run(data_source) +``` \ No newline at end of file diff --git a/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/main.py new file mode 100644 index 00000000..5d242f7c --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/main.py @@ -0,0 +1,27 @@ +from textwrap import dedent + +from ....core.main import Step +from ....core.sdk import ContinueSDK +from ....plugins.steps.core.core import WaitForUserInputStep +from ....plugins.steps.core.core import MessageStep +from .steps import SetUpChessPipelineStep, AddTransformStep + + +class AddTransformRecipe(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + text_observation = await sdk.run_step( + MessageStep(message=dedent("""\ + This recipe will walk you through the process of adding a transform to a dlt pipeline that uses the chess.com API source. With the help of Continue, you will: + - Set up a dlt pipeline for the chess.com API + - Add a filter or map transform to the pipeline + - Run the pipeline and view the transformed data in a Streamlit app"""), name="Add transformation to a dlt pipeline") >> + SetUpChessPipelineStep() >> + WaitForUserInputStep( + prompt="How do you want to transform the Chess.com API data before loading it? For example, you could filter out games that ended in a draw.") + ) + await sdk.run_step( + AddTransformStep( + transform_description=text_observation.text) + ) diff --git a/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/steps.py b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/steps.py new file mode 100644 index 00000000..8c6446da --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/AddTransformRecipe/steps.py @@ -0,0 +1,82 @@ +import os +from textwrap import dedent + +from ....plugins.steps.core.core import MessageStep +from ....core.sdk import Models +from ....core.main import Step +from ....core.sdk import ContinueSDK + +AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" + + +class SetUpChessPipelineStep(Step): + hide: bool = True + name: str = "Setup Chess.com API dlt Pipeline" + + async def describe(self, models: Models): + return "This step will create a new dlt pipeline that loads data from the chess.com API." + + async def run(self, sdk: ContinueSDK): + + # running commands to get started when creating a new dlt pipeline + await sdk.run([ + 'python3 -m venv .env', + 'source .env/bin/activate', + 'pip install dlt', + 'dlt --non-interactive init chess duckdb', + 'pip install -r requirements.txt', + 'pip install pandas streamlit' # Needed for the pipeline show step later + ], name="Set up Python environment", description=dedent(f"""\ + - Create a Python virtual environment: `python3 -m venv .env` + - Activate the virtual environment: `source .env/bin/activate` + - Install dlt: `pip install dlt` + - Create a new dlt pipeline called "chess" that loads data into a local DuckDB instance: `dlt init chess duckdb` + - Install the Python dependencies for the pipeline: `pip install -r requirements.txt`""")) + + +class AddTransformStep(Step): + hide: bool = True + + # e.g. "Use the `python-chess` library to decode the moves in the game data" + transform_description: str + + async def run(self, sdk: ContinueSDK): + source_name = 'chess' + filename = f'{source_name}_pipeline.py' + abs_filepath = os.path.join(sdk.ide.workspace_directory, filename) + + # Open the file and highlight the function to be edited + await sdk.ide.setFileOpen(abs_filepath) + + await sdk.run_step(MessageStep(message=dedent("""\ + This step will customize your resource function with a transform of your choice: + - Add a filter or map transformation depending on your request + - Load the data into a local DuckDB instance + - Open up a Streamlit app for you to view the data"""), name="Write transformation function")) + + with open(os.path.join(os.path.dirname(__file__), 'dlt_transform_docs.md')) as f: + dlt_transform_docs = f.read() + + prompt = dedent(f"""\ + Task: Write a transform function using the description below and then use `add_map` or `add_filter` from the `dlt` library to attach it a resource. + + Description: {self.transform_description} + + Here are some docs pages that will help you better understand how to use `dlt`. + + {dlt_transform_docs}""") + + # edit the pipeline to add a tranform function and attach it to a resource + await sdk.edit_file( + filename=filename, + prompt=prompt, + name=f"Writing transform function {AI_ASSISTED_STRING}" + ) + + await sdk.wait_for_user_confirmation("Press Continue to confirm that the changes are okay before we run the pipeline.") + + # run the pipeline and load the data + await sdk.run(f'python3 {filename}', name="Run the pipeline", description=f"Running `python3 {filename}` to load the data into a local DuckDB instance") + + # run a streamlit app to show the data + await sdk.run(f'dlt pipeline {source_name}_pipeline show', name="Show data in a Streamlit app", description=f"Running `dlt pipeline {source_name} show` to show the data in a Streamlit app, where you can view and play with the data.") diff --git a/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/README.md new file mode 100644 index 00000000..df66104f --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/README.md @@ -0,0 +1,7 @@ +# ContinueRecipeRecipe + +A recipe for building recipes! + +## How to use this recipe + +This recipe takes a single input, a description of the recipe to be built. diff --git a/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/main.py new file mode 100644 index 00000000..c0f9e7e3 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/ContinueRecipeRecipe/main.py @@ -0,0 +1,36 @@ +from textwrap import dedent +from ....plugins.steps.main import EditHighlightedCodeStep +from ....core.main import Step +from ....core.sdk import ContinueSDK + + +class ContinueStepStep(Step): + name: str = "Write your own Continue Step." + prompt: str + + async def run(self, sdk: ContinueSDK): + await sdk.run_step(EditHighlightedCodeStep(user_input=dedent(f"""\ + Here is an example of a Step that runs a command and then edits a file. + + ```python + from ...core.main import Step + from ...core.sdk import ContinueSDK + + class RunCommandAndEditFileStep(Step): + name: str = "Run a command and then edit a file." + command: str + file_path: str + prompt: str + + async def run(self, sdk: ContinueSDK): + await sdk.run([command]) + await sdk.edit_file(filename=self.file_path, prompt=self.prompt) + ``` + + Please edit the code to write your own Step that does the following: + + {self.prommpt} + + It should be a subclass of Step as above, implementing the `run` method, and using pydantic attributes to define the parameters. + + """))) diff --git a/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/README.md new file mode 100644 index 00000000..e69de29b diff --git a/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/main.py new file mode 100644 index 00000000..84363e02 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/main.py @@ -0,0 +1,33 @@ +from textwrap import dedent + +from ....core.sdk import ContinueSDK +from ....core.main import Step +from ....plugins.steps.core.core import WaitForUserInputStep +from ....plugins.steps.core.core import MessageStep +from .steps import SetupPipelineStep, ValidatePipelineStep, RunQueryStep + + +class CreatePipelineRecipe(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + text_observation = await sdk.run_step( + MessageStep(name="Building your first dlt pipeline", message=dedent("""\ + This recipe will walk you through the process of creating a dlt pipeline for your chosen data source. With the help of Continue, you will: + - Create a Python virtual environment with dlt installed + - Run `dlt init` to generate a pipeline template + - Write the code to call the API + - Add any required API keys to the `secrets.toml` file + - Test that the API call works + - Load the data into a local DuckDB instance + - Write a query to view the data""")) >> + WaitForUserInputStep( + prompt="What API do you want to load data from? (e.g. weatherapi.com, chess.com)") + ) + await sdk.run_step( + SetupPipelineStep(api_description=text_observation.text) >> + ValidatePipelineStep() >> + RunQueryStep() >> + MessageStep( + name="Congrats!", message="You've successfully created your first dlt pipeline! 🎉") + ) diff --git a/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py new file mode 100644 index 00000000..433e309e --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py @@ -0,0 +1,168 @@ +import os +from textwrap import dedent +import time + +from ....models.main import Range +from ....models.filesystem import RangeInFile +from ....plugins.steps.core.core import MessageStep +from ....models.filesystem_edit import AddFile, FileEdit +from ....core.main import Step +from ....core.sdk import ContinueSDK, Models + +AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" + + +class SetupPipelineStep(Step): + hide: bool = True + name: str = "Setup dlt Pipeline" + + api_description: str # e.g. "I want to load data from the weatherapi.com API" + + async def describe(self, models: Models): + return dedent(f"""\ + This step will create a new dlt pipeline that loads data from an API, as per your request: + {self.api_description} + """) + + async def run(self, sdk: ContinueSDK): + sdk.context.set("api_description", self.api_description) + + source_name = (await sdk.models.gpt35.complete( + f"Write a snake_case name for the data source described by {self.api_description}: ")).strip() + filename = f'{source_name}.py' + + # running commands to get started when creating a new dlt pipeline + await sdk.run([ + 'python3 -m venv .env', + 'source .env/bin/activate', + 'pip install dlt', + f'dlt --non-interactive init {source_name} duckdb', + 'pip install -r requirements.txt' + ], description=dedent(f"""\ + Running the following commands: + - `python3 -m venv .env`: Create a Python virtual environment + - `source .env/bin/activate`: Activate the virtual environment + - `pip install dlt`: Install dlt + - `dlt init {source_name} duckdb`: Create a new dlt pipeline called {source_name} that loads data into a local DuckDB instance + - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline"""), name="Setup Python environment") + + # editing the resource function to call the requested API + resource_function_range = Range.from_shorthand(15, 0, 30, 0) + await sdk.ide.highlightCode(RangeInFile(filepath=os.path.join(await sdk.ide.getWorkspaceDirectory(), filename), range=resource_function_range), "#ffa50033") + + # sdk.set_loading_message("Writing code to call the API...") + await sdk.edit_file( + range=resource_function_range, + filename=filename, + prompt=f'Edit the resource function to call the API described by this: {self.api_description}. Do not move or remove the exit() call in __main__.', + name=f"Edit the resource function to call the API {AI_ASSISTED_STRING}" + ) + + time.sleep(1) + + # wait for user to put API key in secrets.toml + await sdk.ide.setFileOpen(await sdk.ide.getWorkspaceDirectory() + "/.dlt/secrets.toml") + await sdk.wait_for_user_confirmation("If this service requires an API key, please add it to the `secrets.toml` file and then press `Continue`.") + + sdk.context.set("source_name", source_name) + + +class ValidatePipelineStep(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + workspace_dir = await sdk.ide.getWorkspaceDirectory() + source_name = sdk.context.get("source_name") + filename = f'{source_name}.py' + + # await sdk.run_step(MessageStep(name="Validate the pipeline", message=dedent("""\ + # Next, we will validate that your dlt pipeline is working as expected: + # - Test that the API call works + # - Load the data into a local DuckDB instance + # - Write a query to view the data + # """))) + + # test that the API call works + output = await sdk.run(f'python3 {filename}', name="Test the pipeline", description=f"Running `python3 {filename}` to test loading data from the API", handle_error=False) + + # If it fails, return the error + if "Traceback" in output or "SyntaxError" in output: + output = "Traceback" + output.split("Traceback")[-1] + file_content = await sdk.ide.readFile(os.path.join(workspace_dir, filename)) + suggestion = await sdk.models.gpt35.complete(dedent(f"""\ + ```python + {file_content} + ``` + This above code is a dlt pipeline that loads data from an API. The function with the @resource decorator is responsible for calling the API and returning the data. While attempting to run the pipeline, the following error occurred: + + ```ascii + {output} + ``` + + This is a brief summary of the error followed by a suggestion on how it can be fixed by editing the resource function:""")) + + api_documentation_url = await sdk.models.gpt35.complete(dedent(f"""\ + The API I am trying to call is the '{sdk.context.get('api_description')}'. I tried calling it in the @resource function like this: + ```python + {file_content} + ``` + What is the URL for the API documentation that will help me learn how to make this call? Please format in markdown so I can click the link.""")) + + sdk.raise_exception( + title=f"Error while running pipeline.\nFix the resource function in {filename} and rerun this step", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=dedent(f"""\ + {suggestion} + + {api_documentation_url} + + After you've fixed the code, click the retry button at the top of the Validate Pipeline step above."""))) + + # remove exit() from the main main function + await sdk.run_step(MessageStep(name="Remove early exit() from main function", message="Remove the early exit() from the main function now that we are done testing and want the pipeline to load the data into DuckDB.")) + + contents = await sdk.ide.readFile(os.path.join(workspace_dir, filename)) + replacement = "\n".join( + list(filter(lambda line: line.strip() != "exit()", contents.split("\n")))) + await sdk.ide.applyFileSystemEdit(FileEdit( + filepath=os.path.join(workspace_dir, filename), + replacement=replacement, + range=Range.from_entire_file(contents) + )) + + # load the data into the DuckDB instance + await sdk.run(f'python3 {filename}', name="Load data into DuckDB", description=f"Running python3 {filename} to load data into DuckDB") + + tables_query_code = dedent(f'''\ + import duckdb + + # connect to DuckDB instance + conn = duckdb.connect(database="{source_name}.duckdb") + + # list all tables + print(conn.sql("DESCRIBE"))''') + + query_filename = os.path.join(workspace_dir, "query.py") + await sdk.apply_filesystem_edit(AddFile(filepath=query_filename, content=tables_query_code), name="Add query.py file", description="Adding a file called `query.py` to the workspace that will run a test query on the DuckDB instance") + + +class RunQueryStep(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + output = await sdk.run('.env/bin/python3 query.py', name="Run test query", description="Running `.env/bin/python3 query.py` to test that the data was loaded into DuckDB as expected", handle_error=False) + + if "Traceback" in output or "SyntaxError" in output: + suggestion = await sdk.models.gpt35.complete(dedent(f"""\ + ```python + {await sdk.ide.readFile(os.path.join(sdk.ide.workspace_directory, "query.py"))} + ``` + This above code is a query that runs on the DuckDB instance. While attempting to run the query, the following error occurred: + + ```ascii + {output} + ``` + + This is a brief summary of the error followed by a suggestion on how it can be fixed:""")) + + sdk.raise_exception( + title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion + "\n\nIt is also very likely that no duckdb table was created, which can happen if the resource function did not yield any data. Please make sure that it is yielding data and then rerun this step.") + ) diff --git a/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/README.md new file mode 100644 index 00000000..c4981e56 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/README.md @@ -0,0 +1,3 @@ +# DDtoBQRecipe + +Move from using DuckDB to Google BigQuery as the destination for your `dlt` pipeline \ No newline at end of file diff --git a/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md new file mode 100644 index 00000000..eb68e117 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md @@ -0,0 +1,85 @@ +### Credentials Missing: ConfigFieldMissingException + +You'll see this exception if `dlt` cannot find your bigquery credentials. In the exception below all of them ('project_id', 'private_key', 'client_email') are missing. The exception gives you also the list of all lookups for configuration performed - [here we explain how to read such list](run-a-pipeline.md#missing-secret-or-configuration-values). + +``` +dlt.common.configuration.exceptions.ConfigFieldMissingException: Following fields are missing: ['project_id', 'private_key', 'client_email'] in configuration with spec GcpServiceAccountCredentials + for field "project_id" config providers and keys were tried in following order: + In Environment Variables key WEATHERAPI__DESTINATION__BIGQUERY__CREDENTIALS__PROJECT_ID was not found. + In Environment Variables key WEATHERAPI__DESTINATION__CREDENTIALS__PROJECT_ID was not found. +``` + +The most common cases for the exception: + +1. The secrets are not in `secrets.toml` at all +2. The are placed in wrong section. For example the fragment below will not work: + +```toml +[destination.bigquery] +project_id = "project_id" # please set me up! +``` + +3. You run the pipeline script from the **different** folder from which it is saved. For example `python weatherapi_demo/weatherapi.py` will run the script from `weatherapi_demo` folder but the current working directory is folder above. This prevents `dlt` from finding `weatherapi_demo/.dlt/secrets.toml` and filling-in credentials. + +### Placeholders still in secrets.toml + +Here BigQuery complain that the format of the `private_key` is incorrect. Practically this most often happens if you forgot to replace the placeholders in `secrets.toml` with real values + +``` + +Connection with BigQuerySqlClient to dataset name weatherapi_data failed. Please check if you configured the credentials at all and provided the right credentials values. You can be also denied access or your internet connection may be down. The actual reason given is: No key could be detected. +``` + +### Bigquery not enabled + +[You must enable Bigquery API.](https://console.cloud.google.com/apis/dashboard) + +``` + +403 POST https://bigquery.googleapis.com/bigquery/v2/projects/bq-walkthrough/jobs?prettyPrint=false: BigQuery API has not been used in project 364286133232 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/bigquery.googleapis.com/overview?project=364286133232 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. + +Location: EU +Job ID: a5f84253-3c10-428b-b2c8-1a09b22af9b2 + [{'@type': 'type.googleapis.com/google.rpc.Help', 'links': [{'description': 'Google developers console API activation', 'url': 'https://console.developers.google.com/apis/api/bigquery.googleapis.com/overview?project=364286133232'}]}, {'@type': 'type.googleapis.com/google.rpc.ErrorInfo', 'reason': 'SERVICE_DISABLED', 'domain': 'googleapis.com', 'metadata': {'service': 'bigquery.googleapis.com', 'consumer': 'projects/364286133232'}}] +``` + +### Lack of permissions to create jobs + +Add `BigQuery Job User` as described in the [destination page](../destinations/bigquery.md). + +``` + +403 POST https://bigquery.googleapis.com/bigquery/v2/projects/bq-walkthrough/jobs?prettyPrint=false: Access Denied: Project bq-walkthrough: User does not have bigquery.jobs.create permission in project bq-walkthrough. + +Location: EU +Job ID: c1476d2c-883c-43f7-a5fe-73db195e7bcd +``` + +### Lack of permissions to query/write data + +Add `BigQuery Data Editor` as described in the [destination page](../destinations/bigquery.md). + +``` + +403 Access Denied: Table bq-walkthrough:weatherapi_data._dlt_loads: User does not have permission to query table bq-walkthrough:weatherapi_data._dlt_loads, or perhaps it does not exist in location EU. + +Location: EU +Job ID: 299a92a3-7761-45dd-a433-79fdeb0c1a46 +``` + +### Lack of billing / BigQuery in sandbox mode + +`dlt` does not support BigQuery when project has no billing enabled. If you see a stack trace where following warning appears: + +``` + +403 Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. DML queries are not allowed in the free tier. Set up a billing account to remove this restriction. +``` + +or + +``` +2023-06-08 16:16:26,769|[WARNING ]|8096|dlt|load.py|complete_jobs:198|Job for weatherapi_resource_83b8ac9e98_4_jsonl retried in load 1686233775.932288 with message {"error_result":{"reason":"billingNotEnabled","message":"Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. Table expiration time must be less than 60 days while in sandbox mode."},"errors":[{"reason":"billingNotEnabled","message":"Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. Table expiration time must be less than 60 days while in sandbox mode."}],"job_start":"2023-06-08T14:16:26.850000Z","job_end":"2023-06-08T14:16:26.850000Z","job_id":"weatherapi_resource_83b8ac9e98_4_jsonl"} +``` + +you must enable the billing. diff --git a/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/main.py new file mode 100644 index 00000000..5b6aa8f0 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/main.py @@ -0,0 +1,26 @@ +from textwrap import dedent + +from ....core.main import Step +from ....core.sdk import ContinueSDK +from ....plugins.steps.core.core import MessageStep +from .steps import SetUpChessPipelineStep, SwitchDestinationStep, LoadDataStep + +# Based on the following guide: +# https://github.com/dlt-hub/dlt/pull/392 + + +class DDtoBQRecipe(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + await sdk.run_step( + MessageStep(name="Move from using DuckDB to Google BigQuery as the destination", message=dedent("""\ + This recipe will walk you through the process of moving from using DuckDB to Google BigQuery as the destination for your dlt pipeline. With the help of Continue, you will: + - Set up a dlt pipeline for the chess.com API + - Switch destination from DuckDB to Google BigQuery + - Add BigQuery credentials to your secrets.toml file + - Run the pipeline again to load data to BigQuery""")) >> + SetUpChessPipelineStep() >> + SwitchDestinationStep() >> + LoadDataStep() + ) diff --git a/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/steps.py b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/steps.py new file mode 100644 index 00000000..767936b8 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DDtoBQRecipe/steps.py @@ -0,0 +1,94 @@ +import os +from textwrap import dedent + +from ....plugins.steps.find_and_replace import FindAndReplaceStep +from ....plugins.steps.core.core import MessageStep +from ....core.sdk import Models +from ....core.main import Step +from ....core.sdk import ContinueSDK + +AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" + + +class SetUpChessPipelineStep(Step): + hide: bool = True + name: str = "Setup Chess.com API dlt Pipeline" + + async def describe(self, models: Models): + return "This step will create a new dlt pipeline that loads data from the chess.com API." + + async def run(self, sdk: ContinueSDK): + + # running commands to get started when creating a new dlt pipeline + await sdk.run([ + 'python3 -m venv .env', + 'source .env/bin/activate', + 'pip install dlt', + 'dlt --non-interactive init chess duckdb', + 'pip install -r requirements.txt', + ], name="Set up Python environment", description=dedent(f"""\ + Running the following commands: + - `python3 -m venv .env`: Create a Python virtual environment + - `source .env/bin/activate`: Activate the virtual environment + - `pip install dlt`: Install dlt + - `dlt init chess duckdb`: Create a new dlt pipeline called "chess" that loads data into a local DuckDB instance + - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline""")) + + +class SwitchDestinationStep(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + + # Switch destination from DuckDB to Google BigQuery + filepath = os.path.join( + sdk.ide.workspace_directory, 'chess_pipeline.py') + await sdk.run_step(FindAndReplaceStep(filepath=filepath, pattern="destination='duckdb'", replacement="destination='bigquery'")) + + # Add BigQuery credentials to your secrets.toml file + template = dedent(f"""\ + [destination.bigquery.credentials] + location = "US" # change the location of the data + project_id = "project_id" # please set me up! + private_key = "private_key" # please set me up! + client_email = "client_email" # please set me up!""") + + # wait for user to put API key in secrets.toml + secrets_path = os.path.join( + sdk.ide.workspace_directory, ".dlt/secrets.toml") + await sdk.ide.setFileOpen(secrets_path) + await sdk.append_to_file(secrets_path, template) + + # append template to bottom of secrets.toml + await sdk.wait_for_user_confirmation("Please add your GCP credentials to `secrets.toml` file and then press `Continue`") + + +class LoadDataStep(Step): + name: str = "Load data to BigQuery" + hide: bool = True + + async def run(self, sdk: ContinueSDK): + # Run the pipeline again to load data to BigQuery + output = await sdk.run('.env/bin/python3 chess_pipeline.py', name="Load data to BigQuery", description="Running `.env/bin/python3 chess_pipeline.py` to load data to Google BigQuery") + + if "Traceback" in output or "SyntaxError" in output: + with open(os.path.join(os.path.dirname(__file__), "dlt_duckdb_to_bigquery_docs.md"), "r") as f: + docs = f.read() + + output = "Traceback" + output.split("Traceback")[-1] + suggestion = await sdk.models.default.complete(dedent(f"""\ + When trying to load data into BigQuery, the following error occurred: + + ```ascii + {output} + ``` + + Here is documentation describing common errors and their causes/solutions: + + {docs} + + This is a brief summary of the error followed by a suggestion on how it can be fixed:""")) + + sdk.raise_exception( + title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion) + ) diff --git a/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/README.md new file mode 100644 index 00000000..e69de29b diff --git a/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/main.py new file mode 100644 index 00000000..54cba45f --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/main.py @@ -0,0 +1,61 @@ +from textwrap import dedent + +from ....plugins.steps.input.nl_multiselect import NLMultiselectStep +from ....core.main import Step +from ....core.sdk import ContinueSDK +from ....plugins.steps.core.core import MessageStep +from .steps import SetupPipelineStep, DeployAirflowStep, RunPipelineStep + + +# https://github.com/dlt-hub/dlt-deploy-template/blob/master/airflow-composer/dag_template.py +# https://www.notion.so/dlthub/Deploy-a-pipeline-with-Airflow-245fd1058652479494307ead0b5565f3 +# 1. What verified pipeline do you want to deploy with Airflow? +# 2. Set up selected verified pipeline +# 3. Deploy selected verified pipeline with Airflow +# 4. Set up Airflow locally? + + +class DeployPipelineAirflowRecipe(Step): + hide: bool = True + + async def run(self, sdk: ContinueSDK): + source_name = await sdk.run_step( + MessageStep(name="Deploying a pipeline to Airflow", message=dedent("""\ + This recipe will show you how to deploy a pipeline to Airflow. With the help of Continue, you will: + - Select a dlt-verified pipeline + - Setup the pipeline + - Deploy it to Airflow + - Optionally, setup Airflow locally""")) >> + NLMultiselectStep( + prompt=dedent("""\ + Which verified pipeline do you want to deploy with Airflow? The options are: + - Asana + - Chess.com + - Facebook Ads + - GitHub + - Google Analytics + - Google Sheets + - HubSpot + - Jira + - Matomo + - Mux + - Notion + - Pipedrive + - Pokemon + - Salesforce + - Shopify + - Strapi + - Stripe + - SQL Database + - Workable + - Zendesk"""), + options=[ + "asana_dlt", "chess", "github", "google_analytics", "google_sheets", "hubspot", "matomo", "pipedrive", "shopify_dlt", "strapi", "zendesk", + "facebook_ads", "jira", "mux", "notion", "pokemon", "salesforce", "stripe_analytics", "sql_database", "workable" + ]) + ) + await sdk.run_step( + SetupPipelineStep(source_name=source_name) >> + RunPipelineStep(source_name=source_name) >> + DeployAirflowStep(source_name=source_name) + ) diff --git a/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/steps.py b/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/steps.py new file mode 100644 index 00000000..83067d52 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/DeployPipelineAirflowRecipe/steps.py @@ -0,0 +1,88 @@ +import os +from textwrap import dedent + +from ....plugins.steps.core.core import MessageStep +from ....core.sdk import Models +from ....core.main import Step +from ....core.sdk import ContinueSDK +from ....plugins.steps.find_and_replace import FindAndReplaceStep + +AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" + + +class SetupPipelineStep(Step): + hide: bool = True + name: str = "Setup dlt Pipeline" + + source_name: str + + async def describe(self, models: Models): + pass + + async def run(self, sdk: ContinueSDK): + await sdk.run([ + 'python3 -m venv .env', + 'source .env/bin/activate', + 'pip install dlt', + f'dlt --non-interactive init {self.source_name} duckdb', + 'pip install -r requirements.txt' + ], description=dedent(f"""\ + Running the following commands: + - `python3 -m venv .env`: Create a Python virtual environment + - `source .env/bin/activate`: Activate the virtual environment + - `pip install dlt`: Install dlt + - `dlt init {self.source_name} duckdb`: Create a new dlt pipeline called {self.source_name} that loads data into a local DuckDB instance + - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline"""), name="Setup Python environment") + + +class RunPipelineStep(Step): + hide: bool = True + name: str = "Run dlt Pipeline" + + source_name: str + + async def describe(self, models: Models): + pass + + async def run(self, sdk: ContinueSDK): + await sdk.run([ + f'python3 {self.source_name}_pipeline.py', + ], description=dedent(f"""\ + Running the command `python3 {self.source_name}_pipeline.py to run the pipeline: """), name="Run dlt pipeline") + + +class DeployAirflowStep(Step): + hide: bool = True + source_name: str + + async def run(self, sdk: ContinueSDK): + + # Run dlt command to deploy pipeline to Airflow + await sdk.run( + ['git init', + f'dlt --non-interactive deploy {self.source_name}_pipeline.py airflow-composer'], + description="Running `dlt deploy airflow` to deploy the dlt pipeline to Airflow", name="Deploy dlt pipeline to Airflow") + + # Get filepaths, open the DAG file + directory = await sdk.ide.getWorkspaceDirectory() + pipeline_filepath = os.path.join( + directory, f"{self.source_name}_pipeline.py") + dag_filepath = os.path.join( + directory, f"dags/dag_{self.source_name}_pipeline.py") + + await sdk.ide.setFileOpen(dag_filepath) + + # Replace the pipeline name and dataset name + await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="'pipeline_name'", replacement=f"'{self.source_name}_pipeline'")) + await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="'dataset_name'", replacement=f"'{self.source_name}_data'")) + await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="pipeline_or_source_script", replacement=f"{self.source_name}_pipeline")) + + # Prompt the user for the DAG schedule + # edit_dag_range = Range.from_shorthand(18, 0, 23, 0) + # await sdk.ide.highlightCode(range_in_file=RangeInFile(filepath=dag_filepath, range=edit_dag_range), color="#33993333") + # response = await sdk.run_step(WaitForUserInputStep(prompt="When would you like this Airflow DAG to run? (e.g. every day, every Monday, every 1st of the month, etc.)")) + # await sdk.edit_file(dag_filepath, prompt=f"Edit the DAG so that it runs at the following schedule: '{response.text}'", + # range=edit_dag_range) + + # Tell the user to check the schedule and fill in owner, email, other default_args + await sdk.run_step(MessageStep(message="Fill in the owner, email, and other default_args in the DAG file with your own personal information. Then the DAG will be ready to run!", name="Fill in default_args")) diff --git a/continuedev/src/continuedev/plugins/recipes/README.md b/continuedev/src/continuedev/plugins/recipes/README.md new file mode 100644 index 00000000..9860b0e2 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/README.md @@ -0,0 +1,19 @@ +# This is a collaborative collection of Continue recipes + +A recipe is technically just a [Step](../steps/README.md), but is intended to be more complex, composed of multiple sub-steps. + +Recipes here will automatically be made available in the [Continue VS Code extension](https://marketplace.visualstudio.com/items?itemName=Continue.continue). + +The `recipes` folder contains all recipes, each with the same structure. **If you wish to create your own recipe, please do the following:** + +1. Create a new subfolder in `recipes`, with the name of your recipe (for example `MyNewRecipe`). +2. Make 2 files in this folder: 1) a `README.md` describing your recipe and how to use it and 2) a `main.py` including a single class with the name of your recipe (e.g. `MyNewRecipe`). +3. Write any utility code other than the main recipe class in a separate file, which you can import in `main.py`. Particularly if you decide to break the recipe into multiple sub-steps, try to keep these separate. + +# Existing Recipes + +`ContinueRecipeRecipe` - Write a Continue recipe with Continue. + +`CreatePipelineRecipe` - Build a dlt pipeline from scratch for an API of your choice. + +`WritePytestsRecipe` - Write Pytest unit tests in a folder adjacent to your Python file. diff --git a/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/README.md new file mode 100644 index 00000000..91d1123b --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/README.md @@ -0,0 +1,7 @@ +# TemplateRecipe + +This folder is a template that you can copy to create your own recipe. + +## How to use this recipe + +Explain here what users should know when using your recipe. What inputs does it have and what actions will it perform? diff --git a/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/main.py new file mode 100644 index 00000000..197abe85 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/TemplateRecipe/main.py @@ -0,0 +1,29 @@ +from typing import Coroutine +from ....core.main import Step, Observation +from ....core.sdk import ContinueSDK +from ....core.sdk import Models + + +class TemplateRecipe(Step): + """ + A simple recipe that appends a print statement to the currently open file. + Use this as a template to create your own! + """ + + # Paremeters for the recipe + name: str + + # A title for the recipe, to be displayed in the GUI + title = "Template Recipe" + + # A description of what the recipe accomplished, to be displayed in the GUI + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return f"Appended a statement to print `Hello, {self.name}!` at the end of the file." + + # The code executed when the recipe is run + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + visible_files = await sdk.ide.getVisibleFiles() + await sdk.edit_file( + filename=visible_files[0], + prompt=f"Append a statement to print `Hello, {self.name}!` at the end of the file." + ) diff --git a/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/README.md b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/README.md new file mode 100644 index 00000000..5ce33ecb --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/README.md @@ -0,0 +1,7 @@ +# CreatePytestsRecipe + +A recipe for writing unit tests in Pytest. + +# How to use this recipe + +Call this recipe with a python file open that you would like to test. It will create tests in a `tests/` folder adjacent to the file with the test file given the same name prepended by `test_`. diff --git a/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py new file mode 100644 index 00000000..6ef5ffd6 --- /dev/null +++ b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py @@ -0,0 +1,49 @@ +from textwrap import dedent +from typing import Union +from ....models.filesystem_edit import AddDirectory, AddFile +from ....core.main import Step +from ....core.sdk import ContinueSDK +import os + + +class WritePytestsRecipe(Step): + for_filepath: Union[str, None] = None + user_input: str = "Write unit tests for this file." + + async def describe(self, models): + return f"Writing unit tests for {self.for_filepath}" + + async def run(self, sdk: ContinueSDK): + if self.for_filepath is None: + self.for_filepath = (await sdk.ide.getVisibleFiles())[0] + + filename = os.path.basename(self.for_filepath) + dirname = os.path.dirname(self.for_filepath) + + path_dir = os.path.join(dirname, "tests") + if not os.path.exists(path_dir): + await sdk.apply_filesystem_edit(AddDirectory(path=path_dir)) + + path = os.path.join(path_dir, f"test_{filename}") + if os.path.exists(path): + return None + + for_file_contents = await sdk.ide.readFile(self.for_filepath) + + prompt = dedent(f"""\ + This is the file you will write unit tests for: + + ```python + {for_file_contents} + ``` + + Here are additional instructions: + + "{self.user_input}" + + Here is a complete set of pytest unit tests:""") + tests = await sdk.models.gpt35.complete(prompt) + + await sdk.apply_filesystem_edit(AddFile(filepath=path, content=tests)) + + return None diff --git a/continuedev/src/continuedev/plugins/steps/README.md b/continuedev/src/continuedev/plugins/steps/README.md new file mode 100644 index 00000000..12073835 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/README.md @@ -0,0 +1,50 @@ +# Steps + +Steps are the composable unit of action in Continue. They define a `run` method which has access to the entire `ContinueSDK`, allowing you to take actions inside the IDE, call language models, and more. In this folder you can find a number of good examples. + +## How to write a step + +a. Start by creating a subclass of `Step` + +You should first consider what will be the parameters of your recipe. These are defined as attributes in the Pydantic class. For example, if you wanted a "filepath" attribute that would look like this: + +```python +class HelloWorldStep(Step): + filepath: str + ... +``` + +b. Next, write the `run` method + +This method takes the ContinueSDK as a parameter, giving you all the tools you need to write your steps (if it's missing something, let us know, we'll add it!). You can write any code inside the run method; this is what will happen when your step is run, line for line. As an example, here's a step that will open a file and append "Hello World!": + +```python +class HelloWorldStep(Step): + filepath: str + + async def run(self, sdk: ContinueSDK): + await sdk.ide.setFileOpen(self.filepath) + await sdk.append_to_file(self.filepath, "Hello World!") +``` + +c. Finally, every Step is displayed with a description of what it has done + +If you'd like to override the default description of your step, which is just the class name, then implement the `describe` method. You can: + +- Return a static string +- Store state in a class attribute (prepend with a double underscore, which signifies (through Pydantic) that this is not a parameter for the Step, just internal state) during the run method, and then grab this in the describe method. +- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.gpt35.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`. + +Here's an example: + +```python +class HelloWorldStep(Step): + filepath: str + + async def run(self, sdk: ContinueSDK): + await sdk.ide.setFileOpen(self.filepath) + await sdk.append_to_file(self.filepath, "Hello World!") + + def describe(self, models: Models): + return f"Appended 'Hello World!' to {self.filepath}" +``` diff --git a/continuedev/src/continuedev/plugins/steps/__init__.py b/continuedev/src/continuedev/plugins/steps/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/__init__.py @@ -0,0 +1 @@ + diff --git a/continuedev/src/continuedev/plugins/steps/chat.py b/continuedev/src/continuedev/plugins/steps/chat.py new file mode 100644 index 00000000..2c662459 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/chat.py @@ -0,0 +1,270 @@ +import json +from typing import Any, Coroutine, List + +from pydantic import Field + +from ...libs.util.strings import remove_quotes_and_escapes +from .main import EditHighlightedCodeStep +from .core.core import MessageStep +from ...core.main import FunctionCall, Models +from ...core.main import ChatMessage, Step, step_to_json_schema +from ...core.sdk import ContinueSDK +import openai +import os +from dotenv import load_dotenv +from directory_tree import display_tree + +load_dotenv() +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +openai.api_key = OPENAI_API_KEY + + +class SimpleChatStep(Step): + name: str = "Generating Response..." + manage_own_chat_context: bool = True + description: str = "" + messages: List[ChatMessage] = None + + async def run(self, sdk: ContinueSDK): + completion = "" + messages = self.messages or await sdk.get_chat_context() + + generator = sdk.models.default.stream_chat( + messages, temperature=sdk.config.temperature) + try: + async for chunk in generator: + if sdk.current_step_was_deleted(): + # So that the message doesn't disappear + self.hide = False + break + + if "content" in chunk: + self.description += chunk["content"] + completion += chunk["content"] + await sdk.update_ui() + finally: + self.name = remove_quotes_and_escapes(await sdk.models.gpt35.complete( + f"Write a short title for the following chat message: {self.description}")) + + self.chat_context.append(ChatMessage( + role="assistant", + content=completion, + summary=self.name + )) + + # TODO: Never actually closing. + await generator.aclose() + + +class AddFileStep(Step): + name: str = "Add File" + description = "Add a file to the workspace. Should always view the directory tree before this." + filename: str + file_contents: str + + async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: + return f"Added a file named `{self.filename}` to the workspace." + + async def run(self, sdk: ContinueSDK): + await sdk.add_file(self.filename, self.file_contents) + + await sdk.ide.setFileOpen(os.path.join(sdk.ide.workspace_directory, self.filename)) + + +class DeleteFileStep(Step): + name: str = "Delete File" + description = "Delete a file from the workspace." + filename: str + + async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: + return f"Deleted a file named `{self.filename}` from the workspace." + + async def run(self, sdk: ContinueSDK): + await sdk.delete_file(self.filename) + + +class AddDirectoryStep(Step): + name: str = "Add Directory" + description = "Add a directory to the workspace." + directory_name: str + + async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: + return f"Added a directory named `{self.directory_name}` to the workspace." + + async def run(self, sdk: ContinueSDK): + try: + await sdk.add_directory(self.directory_name) + except FileExistsError: + self.description = f"Directory {self.directory_name} already exists." + + +class RunTerminalCommandStep(Step): + name: str = "Run Terminal Command" + description: str = "Run a terminal command." + command: str + + async def run(self, sdk: ContinueSDK): + self.description = f"Copy this command and run in your terminal:\n\n```bash\n{self.command}\n```" + + +class ViewDirectoryTreeStep(Step): + name: str = "View Directory Tree" + description: str = "View the directory tree to learn which folder and files exist. You should always do this before adding new files." + + async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: + return f"Viewed the directory tree." + + async def run(self, sdk: ContinueSDK): + self.description = f"```\n{display_tree(sdk.ide.workspace_directory, True, max_depth=2)}\n```" + + +class EditFileStep(Step): + name: str = "Edit File" + description: str = "Edit a file in the workspace that is not currently open." + filename: str = Field( + ..., description="The name of the file to edit.") + instructions: str = Field( + ..., description="The instructions to edit the file.") + hide: bool = True + + async def run(self, sdk: ContinueSDK): + await sdk.edit_file(self.filename, self.instructions) + + +class ChatWithFunctions(Step): + user_input: str + functions: List[Step] = [AddFileStep(filename="", file_contents=""), + EditFileStep(filename="", instructions=""), + EditHighlightedCodeStep(user_input=""), + ViewDirectoryTreeStep(), AddDirectoryStep(directory_name=""), + DeleteFileStep(filename=""), RunTerminalCommandStep(command="")] + name: str = "Input" + manage_own_chat_context: bool = True + description: str = "" + hide: bool = True + + async def run(self, sdk: ContinueSDK): + await sdk.update_ui() + + step_name_step_class_map = { + step.name.replace(" ", ""): step.__class__ for step in self.functions} + + functions = [step_to_json_schema( + function) for function in self.functions] + + self.chat_context.append(ChatMessage( + role="user", + content=self.user_input, + summary=self.user_input + )) + + last_function_called_index_in_history = None + last_function_called_name = None + last_function_called_params = None + while True: + was_function_called = False + func_args = "" + func_name = "" + msg_content = "" + msg_step = None + + async for msg_chunk in sdk.models.gpt350613.stream_chat(await sdk.get_chat_context(), functions=functions): + if sdk.current_step_was_deleted(): + return + + if "content" in msg_chunk and msg_chunk["content"] is not None: + msg_content += msg_chunk["content"] + # if last_function_called_index_in_history is not None: + # while sdk.history.timeline[last_function_called_index].step.hide: + # last_function_called_index += 1 + # sdk.history.timeline[last_function_called_index_in_history].step.description = msg_content + if msg_step is None: + msg_step = MessageStep( + name="Chat", + message=msg_chunk["content"] + ) + await sdk.run_step(msg_step) + else: + msg_step.description = msg_content + await sdk.update_ui() + elif "function_call" in msg_chunk or func_name != "": + was_function_called = True + if "function_call" in msg_chunk: + if "arguments" in msg_chunk["function_call"]: + func_args += msg_chunk["function_call"]["arguments"] + if "name" in msg_chunk["function_call"]: + func_name += msg_chunk["function_call"]["name"] + + if not was_function_called: + self.chat_context.append(ChatMessage( + role="assistant", + content=msg_content, + summary=msg_content + )) + break + else: + last_function_called = func_name + if func_name == "python" and "python" not in step_name_step_class_map: + # GPT must be fine-tuned to believe this exists, but it doesn't always + func_name = "EditHighlightedCodeStep" + func_args = json.dumps({"user_input": self.user_input}) + # self.chat_context.append(ChatMessage( + # role="assistant", + # content=None, + # function_call=FunctionCall( + # name=func_name, + # arguments=func_args + # ), + # summary=f"Called function {func_name}" + # )) + # self.chat_context.append(ChatMessage( + # role="user", + # content="The 'python' function does not exist. Don't call it. Try again to call another function.", + # summary="'python' function does not exist." + # )) + # msg_step.hide = True + # continue + # Call the function, then continue to chat + func_args = "{}" if func_args == "" else func_args + try: + fn_call_params = json.loads(func_args) + except json.JSONDecodeError: + raise Exception( + "The model returned invalid JSON. Please try again") + self.chat_context.append(ChatMessage( + role="assistant", + content=None, + function_call=FunctionCall( + name=func_name, + arguments=func_args + ), + summary=f"Called function {func_name}" + )) + last_function_called_index_in_history = sdk.history.current_index + 1 + if func_name not in step_name_step_class_map: + raise Exception( + f"The model tried to call a function ({func_name}) that does not exist. Please try again.") + + # if func_name == "AddFileStep": + # step_to_run.hide = True + # self.description += f"\nAdded file `{func_args['filename']}`" + # elif func_name == "AddDirectoryStep": + # step_to_run.hide = True + # self.description += f"\nAdded directory `{func_args['directory_name']}`" + # else: + # self.description += f"\n`Running function {func_name}`\n\n" + if func_name == "EditHighlightedCodeStep": + fn_call_params["user_input"] = self.user_input + elif func_name == "EditFile": + fn_call_params["instructions"] = self.user_input + + step_to_run = step_name_step_class_map[func_name]( + **fn_call_params) + if last_function_called_name is not None and last_function_called_name == func_name and last_function_called_params is not None and last_function_called_params == fn_call_params: + # If it's calling the same function more than once in a row, it's probably looping and confused + return + last_function_called_name = func_name + last_function_called_params = fn_call_params + + await sdk.run_step(step_to_run) + await sdk.update_ui() diff --git a/continuedev/src/continuedev/plugins/steps/chroma.py b/continuedev/src/continuedev/plugins/steps/chroma.py new file mode 100644 index 00000000..dbe8363e --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/chroma.py @@ -0,0 +1,80 @@ +from textwrap import dedent +from typing import Coroutine, Union +from ...core.observation import Observation, TextObservation +from ...core.main import Step +from ...core.sdk import ContinueSDK +from .core.core import EditFileStep +from ...libs.chroma.query import ChromaIndexManager +from .core.core import EditFileStep + + +class CreateCodebaseIndexChroma(Step): + name: str = "Create Codebase Index" + hide: bool = True + + async def describe(self, llm) -> Coroutine[str, None, None]: + return "Indexing the codebase..." + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) + if not index.check_index_exists(): + self.hide = False + index.create_codebase_index() + + +class AnswerQuestionChroma(Step): + question: str + _answer: Union[str, None] = None + name: str = "Answer Question" + + async def describe(self, llm) -> Coroutine[str, None, None]: + if self._answer is None: + return f"Answering the question: {self.question}" + else: + return self._answer + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) + results = index.query_codebase_index(self.question) + + code_snippets = "" + + files = [] + for node in results.source_nodes: + resource_name = list(node.node.relationships.values())[0] + filepath = resource_name[:resource_name.index("::")] + files.append(filepath) + code_snippets += f"""{filepath}```\n{node.node.text}\n```\n\n""" + + prompt = dedent(f"""Here are a few snippets of code that might be useful in answering the question: + + {code_snippets} + + Here is the question to answer: + + {self.question} + + Here is the answer:""") + + answer = await sdk.models.gpt35.complete(prompt) + # Make paths relative to the workspace directory + answer = answer.replace(await sdk.ide.getWorkspaceDirectory(), "") + + self._answer = answer + + await sdk.ide.setFileOpen(files[0]) + + +class EditFileChroma(Step): + request: str + hide: bool = True + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) + results = index.query_codebase_index(self.request) + + resource_name = list( + results.source_nodes[0].node.relationships.values())[0] + filepath = resource_name[:resource_name.index("::")] + + await sdk.run_step(EditFileStep(filepath=filepath, prompt=f"Here is the code:\n\n{{code}}\n\nHere is the user request:\n\n{self.request}\n\nHere is the code after making the requested changes:\n")) diff --git a/continuedev/src/continuedev/plugins/steps/clear_history.py b/continuedev/src/continuedev/plugins/steps/clear_history.py new file mode 100644 index 00000000..8f21518b --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/clear_history.py @@ -0,0 +1,10 @@ +from ...core.main import Step +from ...core.sdk import ContinueSDK + + +class ClearHistoryStep(Step): + name: str = "Clear History" + hide: bool = True + + async def run(self, sdk: ContinueSDK): + await sdk.clear_history() diff --git a/continuedev/src/continuedev/plugins/steps/comment_code.py b/continuedev/src/continuedev/plugins/steps/comment_code.py new file mode 100644 index 00000000..3e34ab52 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/comment_code.py @@ -0,0 +1,12 @@ +from ...core.main import ContinueSDK, Models, Step +from .main import EditHighlightedCodeStep + + +class CommentCodeStep(Step): + hide: bool = True + + async def describe(self, models: Models): + return "Writing comments" + + async def run(self, sdk: ContinueSDK): + await sdk.run_step(EditHighlightedCodeStep(user_input="Write comprehensive comments in the canonical format for every class and function")) diff --git a/continuedev/src/continuedev/plugins/steps/core/core.py b/continuedev/src/continuedev/plugins/steps/core/core.py new file mode 100644 index 00000000..5a81e5ee --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/core/core.py @@ -0,0 +1,731 @@ +# These steps are depended upon by ContinueSDK +import os +import subprocess +import difflib +from textwrap import dedent +from typing import Coroutine, List, Literal, Union + +from ....libs.llm.ggml import GGML +from ....models.main import Range +from ....libs.llm.prompt_utils import MarkdownStyleEncoderDecoder +from ....models.filesystem_edit import EditDiff, FileEdit, FileEditWithFullContents, FileSystemEdit +from ....models.filesystem import FileSystem, RangeInFile, RangeInFileWithContents +from ....core.observation import Observation, TextObservation, TracebackObservation, UserInputObservation +from ....core.main import ChatMessage, ContinueCustomException, Step, SequentialStep +from ....libs.util.count_tokens import MAX_TOKENS_FOR_MODEL, DEFAULT_MAX_TOKENS +from ....libs.util.strings import dedent_and_get_common_whitespace, remove_quotes_and_escapes +import difflib + + +class ContinueSDK: + pass + + +class Models: + pass + + +class ReversibleStep(Step): + async def reverse(self, sdk: ContinueSDK): + raise NotImplementedError + + +class MessageStep(Step): + name: str = "Message" + message: str + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return self.message + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + return TextObservation(text=self.message) + + +class FileSystemEditStep(ReversibleStep): + edit: FileSystemEdit + _diff: Union[EditDiff, None] = None + + hide: bool = True + + async def run(self, sdk: "ContinueSDK") -> Coroutine[Observation, None, None]: + self._diff = await sdk.ide.applyFileSystemEdit(self.edit) + return None + + async def reverse(self, sdk: "ContinueSDK"): + await sdk.ide.applyFileSystemEdit(self._diff.backward) + # Where and when should file saves happen? + + +def output_contains_error(output: str) -> bool: + return "Traceback" in output or "SyntaxError" in output + + +AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" + + +class ShellCommandsStep(Step): + cmds: List[str] + cwd: Union[str, None] = None + name: str = "Run Shell Commands" + handle_error: bool = True + + _err_text: Union[str, None] = None + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + if self._err_text is not None: + return f"Error when running shell commands:\n```\n{self._err_text}\n```" + + cmds_str = "\n".join(self.cmds) + return await models.gpt35.complete(f"{cmds_str}\n\nSummarize what was done in these shell commands, using markdown bullet points:") + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + cwd = await sdk.ide.getWorkspaceDirectory() if self.cwd is None else self.cwd + + for cmd in self.cmds: + output = await sdk.ide.runCommand(cmd) + if self.handle_error and output is not None and output_contains_error(output): + suggestion = await sdk.models.gpt35.complete(dedent(f"""\ + While running the command `{cmd}`, the following error occurred: + + ```ascii + {output} + ``` + + This is a brief summary of the error followed by a suggestion on how it can be fixed:"""), with_history=await sdk.get_chat_context()) + + sdk.raise_exception( + title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=f"{suggestion}\n\nYou can click the retry button on the failed step to try again.") + ) + + return TextObservation(text=output) + + # process = subprocess.Popen( + # '/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd) + + # stdin_input = "\n".join(self.cmds) + # out, err = process.communicate(stdin_input.encode()) + + # # If it fails, return the error + # if err is not None and err != "": + # self._err_text = err + # return TextObservation(text=err) + + # return None + + +class DefaultModelEditCodeStep(Step): + user_input: str + range_in_files: List[RangeInFile] + name: str = "Editing Code" + hide = False + description: str = "" + _prompt: str = dedent("""\ + Take the file prefix and suffix into account, but only rewrite the code_to_edit as specified in the user_request. The code you write in modified_code_to_edit will replace the code between the code_to_edit tags. Do NOT preface your answer or write anything other than code. The tag should be written to indicate the end of the modified code section. Do not ever use nested tags. + + Example: + + + class Database: + def __init__(self): + self._data = {{}} + + def get(self, key): + return self._data[key] + + + + def set(self, key, value): + self._data[key] = value + + + + def clear_all(): + self._data = {{}} + + + Raise an error if the key already exists. + + + def set(self, key, value): + if key in self._data: + raise KeyError(f"Key {{key}} already exists") + self._data[key] = value + + + Main task: + """) + _previous_contents: str = "" + _new_contents: str = "" + _prompt_and_completion: str = "" + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + if self._previous_contents.strip() == self._new_contents.strip(): + description = "No edits were made" + else: + changes = '\n'.join(difflib.ndiff( + self._previous_contents.splitlines(), self._new_contents.splitlines())) + description = await models.gpt3516k.complete(dedent(f"""\ + Diff summary: "{self.user_input}" + + ```diff + {changes} + ``` + + Please give brief a description of the changes made above using markdown bullet points. Be concise:""")) + name = await models.gpt3516k.complete(f"Write a very short title to describe this requested change (no quotes): '{self.user_input}'. This is the title:") + self.name = remove_quotes_and_escapes(name) + + return f"{remove_quotes_and_escapes(description)}" + + async def get_prompt_parts(self, rif: RangeInFileWithContents, sdk: ContinueSDK, full_file_contents: str): + # We don't know here all of the functions being passed in. + # We care because if this prompt itself goes over the limit, then the entire message will have to be cut from the completion. + # Overflow won't happen, but prune_chat_messages in count_tokens.py will cut out this whole thing, instead of us cutting out only as many lines as we need. + model_to_use = sdk.models.default + max_tokens = int(MAX_TOKENS_FOR_MODEL.get( + model_to_use.name, DEFAULT_MAX_TOKENS) / 2) + + TOKENS_TO_BE_CONSIDERED_LARGE_RANGE = 1200 + if model_to_use.count_tokens(rif.contents) > TOKENS_TO_BE_CONSIDERED_LARGE_RANGE: + self.description += "\n\n**It looks like you've selected a large range to edit, which may take a while to complete. If you'd like to cancel, click the 'X' button above. If you highlight a more specific range, Continue will only edit within it.**" + + # At this point, we also increase the max_tokens parameter so it doesn't stop in the middle of generation + # Increase max_tokens to be double the size of the range + # But don't exceed twice default max tokens + max_tokens = int(min(model_to_use.count_tokens( + rif.contents), DEFAULT_MAX_TOKENS) * 2.5) + + BUFFER_FOR_FUNCTIONS = 400 + total_tokens = model_to_use.count_tokens( + full_file_contents + self._prompt + self.user_input) + BUFFER_FOR_FUNCTIONS + max_tokens + + # If using 3.5 and overflows, upgrade to 3.5.16k + if model_to_use.name == "gpt-3.5-turbo": + if total_tokens > MAX_TOKENS_FOR_MODEL["gpt-3.5-turbo"]: + model_to_use = sdk.models.gpt3516k + + # Remove tokens from the end first, and then the start to clear space + # This part finds the start and end lines + full_file_contents_lst = full_file_contents.split("\n") + max_start_line = rif.range.start.line + min_end_line = rif.range.end.line + cur_start_line = 0 + cur_end_line = len(full_file_contents_lst) - 1 + + if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]: + while cur_end_line > min_end_line: + total_tokens -= model_to_use.count_tokens( + full_file_contents_lst[cur_end_line]) + cur_end_line -= 1 + if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]: + break + + if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]: + while cur_start_line < max_start_line: + cur_start_line += 1 + total_tokens -= model_to_use.count_tokens( + full_file_contents_lst[cur_start_line]) + if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]: + break + + # Now use the found start/end lines to get the prefix and suffix strings + file_prefix = "\n".join( + full_file_contents_lst[cur_start_line:max_start_line]) + file_suffix = "\n".join( + full_file_contents_lst[min_end_line:cur_end_line - 1]) + + # Move any surrounding blank line in rif.contents to the prefix/suffix + # TODO: Keep track of start line of the range, because it's needed below for offset stuff + rif_start_line = rif.range.start.line + if len(rif.contents) > 0: + lines = rif.contents.splitlines(keepends=True) + first_line = lines[0] if lines else None + while first_line and first_line.strip() == "": + file_prefix += first_line + rif.contents = rif.contents[len(first_line):] + lines = rif.contents.splitlines(keepends=True) + first_line = lines[0] if lines else None + + last_line = lines[-1] if lines else None + while last_line and last_line.strip() == "": + file_suffix = last_line + file_suffix + rif.contents = rif.contents[:len( + rif.contents) - len(last_line)] + lines = rif.contents.splitlines(keepends=True) + last_line = lines[-1] if lines else None + + while rif.contents.startswith("\n"): + file_prefix += "\n" + rif.contents = rif.contents[1:] + while rif.contents.endswith("\n"): + file_suffix = "\n" + file_suffix + rif.contents = rif.contents[:-1] + + return file_prefix, rif.contents, file_suffix, model_to_use, max_tokens + + def compile_prompt(self, file_prefix: str, contents: str, file_suffix: str, sdk: ContinueSDK) -> str: + if contents.strip() == "": + # Seperate prompt for insertion at the cursor, the other tends to cause it to repeat whole file + prompt = dedent(f"""\ + +{file_prefix} + + + +{file_suffix} + + +{self.user_input} + + +Please output the code to be inserted at the cursor in order to fulfill the user_request. Do NOT preface your answer or write anything other than code. You should not write any tags, just the code. Make sure to correctly indent the code:""") + return prompt + + prompt = self._prompt + if file_prefix.strip() != "": + prompt += dedent(f""" + +{file_prefix} +""") + prompt += dedent(f""" + +{contents} +""") + if file_suffix.strip() != "": + prompt += dedent(f""" + +{file_suffix} +""") + prompt += dedent(f""" + +{self.user_input} + + +""") + + return prompt + + def is_end_line(self, line: str) -> bool: + return "" in line or "" in line + + def line_to_be_ignored(self, line: str, is_first_line: bool = False) -> bool: + return "```" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line + + async def stream_rif(self, rif: RangeInFileWithContents, sdk: ContinueSDK): + await sdk.ide.saveFile(rif.filepath) + full_file_contents = await sdk.ide.readFile(rif.filepath) + + file_prefix, contents, file_suffix, model_to_use, max_tokens = await self.get_prompt_parts( + rif, sdk, full_file_contents) + contents, common_whitespace = dedent_and_get_common_whitespace( + contents) + prompt = self.compile_prompt(file_prefix, contents, file_suffix, sdk) + full_file_contents_lines = full_file_contents.split("\n") + + lines_to_display = [] + + async def sendDiffUpdate(lines: List[str], sdk: ContinueSDK, final: bool = False): + nonlocal full_file_contents_lines, rif, lines_to_display + + completion = "\n".join(lines) + + full_prefix_lines = full_file_contents_lines[:rif.range.start.line] + full_suffix_lines = full_file_contents_lines[rif.range.end.line:] + + # Don't do this at the very end, just show the inserted code + if final: + lines_to_display = [] + # Only recalculate at every new-line, because this is sort of expensive + elif completion.endswith("\n"): + contents_lines = rif.contents.split("\n") + rewritten_lines = 0 + for line in lines: + for i in range(rewritten_lines, len(contents_lines)): + if difflib.SequenceMatcher(None, line, contents_lines[i]).ratio() > 0.7 and contents_lines[i].strip() != "": + rewritten_lines = i + 1 + break + lines_to_display = contents_lines[rewritten_lines:] + + new_file_contents = "\n".join( + full_prefix_lines) + "\n" + completion + "\n" + ("\n".join(lines_to_display) + "\n" if len(lines_to_display) > 0 else "") + "\n".join(full_suffix_lines) + + step_index = sdk.history.current_index + + await sdk.ide.showDiff(rif.filepath, new_file_contents, step_index) + + # Important state variables + # ------------------------- + original_lines = [] if rif.contents == "" else rif.contents.split("\n") + # In the actual file, taking into account block offset + current_line_in_file = rif.range.start.line + current_block_lines = [] + original_lines_below_previous_blocks = original_lines + # The start of the current block in file, taking into account block offset + current_block_start = -1 + offset_from_blocks = 0 + + # Don't end the block until you've matched N simultaneous lines + # This helps avoid many tiny blocks + LINES_TO_MATCH_BEFORE_ENDING_BLOCK = 2 + # If a line has been matched at the end of the block, this is its index within original_lines_below_previous_blocks + # Except we are keeping track of multiple potentialities, so it's a list + # We always check the lines following each of these leads, but if multiple make it out at the end, we use the first one + # This is a tuple of (index_of_last_matched_line, number_of_lines_matched) + indices_of_last_matched_lines = [] + + async def handle_generated_line(line: str): + nonlocal current_block_start, current_line_in_file, original_lines, original_lines_below_previous_blocks, current_block_lines, indices_of_last_matched_lines, LINES_TO_MATCH_BEFORE_ENDING_BLOCK, offset_from_blocks + + # Highlight the line to show progress + line_to_highlight = current_line_in_file - len(current_block_lines) + if False: + await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=Range.from_shorthand( + line_to_highlight, 0, line_to_highlight, 0)), "#FFFFFF22" if len(current_block_lines) == 0 else "#00FF0022") + + if len(current_block_lines) == 0: + # Set this as the start of the next block + current_block_start = rif.range.start.line + len(original_lines) - len( + original_lines_below_previous_blocks) + offset_from_blocks + if len(original_lines_below_previous_blocks) > 0 and line == original_lines_below_previous_blocks[0]: + # Line is equal to the next line in file, move past this line + original_lines_below_previous_blocks = original_lines_below_previous_blocks[ + 1:] + return + + # In a block, and have already matched at least one line + # Check if the next line matches, for each of the candidates + matches_found = [] + first_valid_match = None + for index_of_last_matched_line, num_lines_matched in indices_of_last_matched_lines: + if index_of_last_matched_line + 1 < len(original_lines_below_previous_blocks) and line == original_lines_below_previous_blocks[index_of_last_matched_line + 1]: + matches_found.append( + (index_of_last_matched_line + 1, num_lines_matched + 1)) + if first_valid_match is None and num_lines_matched + 1 >= LINES_TO_MATCH_BEFORE_ENDING_BLOCK: + first_valid_match = ( + index_of_last_matched_line + 1, num_lines_matched + 1) + indices_of_last_matched_lines = matches_found + + if first_valid_match is not None: + # We've matched the required number of lines, insert suggestion! + + # We added some lines to the block that were matched (including maybe some blank lines) + # So here we will strip all matching lines from the end of current_block_lines + lines_stripped = [] + index_of_last_line_in_block = first_valid_match[0] + while len(current_block_lines) > 0 and current_block_lines[-1] == original_lines_below_previous_blocks[index_of_last_line_in_block - 1]: + lines_stripped.append(current_block_lines.pop()) + index_of_last_line_in_block -= 1 + + # It's also possible that some lines match at the beginning of the block + # lines_stripped_at_beginning = [] + # j = 0 + # while len(current_block_lines) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[first_valid_match[0] - first_valid_match[1] + j]: + # lines_stripped_at_beginning.append( + # current_block_lines.pop(0)) + # j += 1 + # # current_block_start += 1 + + # Insert the suggestion + replacement = "\n".join(current_block_lines) + start_line = current_block_start + end_line = current_block_start + index_of_last_line_in_block + + if False: + await sdk.ide.showSuggestion(FileEdit( + filepath=rif.filepath, + range=Range.from_shorthand( + start_line, 0, end_line, 0), + replacement=replacement + )) + + # Reset current block / update variables + current_line_in_file += 1 + offset_from_blocks += len(current_block_lines) + original_lines_below_previous_blocks = original_lines_below_previous_blocks[ + index_of_last_line_in_block + 1:] + current_block_lines = [] + current_block_start = -1 + indices_of_last_matched_lines = [] + + return + + # Always look for new matching candidates + new_matches = [] + for i in range(len(original_lines_below_previous_blocks)): + og_line = original_lines_below_previous_blocks[i] + # TODO: It's a bit sus to be disqualifying empty lines. + # What you ideally do is find ALL matches, and then throw them out as you check the following lines + if og_line == line: # and og_line.strip() != "": + new_matches.append((i, 1)) + indices_of_last_matched_lines += new_matches + + # Make sure they are sorted by index + indices_of_last_matched_lines = sorted( + indices_of_last_matched_lines, key=lambda x: x[0]) + + current_block_lines.append(line) + + messages = await sdk.get_chat_context() + # Delete the last user and assistant messages + i = len(messages) - 1 + deleted = 0 + while i >= 0 and deleted < 2: + if messages[i].role == "user" or messages[i].role == "assistant": + messages.pop(i) + deleted += 1 + i -= 1 + messages.append(ChatMessage( + role="user", + content=prompt, + summary=self.user_input + )) + + lines_of_prefix_copied = 0 + lines = [] + unfinished_line = "" + completion_lines_covered = 0 + repeating_file_suffix = False + line_below_highlighted_range = file_suffix.lstrip().split("\n")[0] + + if isinstance(model_to_use, GGML): + messages = [ChatMessage( + role="user", content=f"```\n{rif.contents}\n```\n\nUser request: \"{self.user_input}\"\n\nThis is the code after changing to perfectly comply with the user request. It does not include any placeholder code, only real implementations:\n\n```\n", summary=self.user_input)] + + generator = model_to_use.stream_chat( + messages, temperature=sdk.config.temperature, max_tokens=max_tokens) + + try: + async for chunk in generator: + # Stop early if it is repeating the file_suffix or the step was deleted + if repeating_file_suffix: + break + if sdk.current_step_was_deleted(): + return + + # Accumulate lines + if "content" not in chunk: + continue + chunk = chunk["content"] + chunk_lines = chunk.split("\n") + chunk_lines[0] = unfinished_line + chunk_lines[0] + if chunk.endswith("\n"): + unfinished_line = "" + chunk_lines.pop() # because this will be an empty string + else: + unfinished_line = chunk_lines.pop() + + # Deal with newly accumulated lines + for i in range(len(chunk_lines)): + # Trailing whitespace doesn't matter + chunk_lines[i] = chunk_lines[i].rstrip() + chunk_lines[i] = common_whitespace + chunk_lines[i] + + # Lines that should signify the end of generation + if self.is_end_line(chunk_lines[i]): + break + # Lines that should be ignored, like the <> tags + elif self.line_to_be_ignored(chunk_lines[i], completion_lines_covered == 0): + continue + # Check if we are currently just copying the prefix + elif (lines_of_prefix_copied > 0 or completion_lines_covered == 0) and lines_of_prefix_copied < len(file_prefix.splitlines()) and chunk_lines[i] == full_file_contents_lines[lines_of_prefix_copied]: + # This is a sketchy way of stopping it from repeating the file_prefix. Is a bug if output happens to have a matching line + lines_of_prefix_copied += 1 + continue + # Because really short lines might be expected to be repeated, this is only a !heuristic! + # Stop when it starts copying the file_suffix + elif chunk_lines[i].strip() == line_below_highlighted_range.strip() and len(chunk_lines[i].strip()) > 4 and not (len(original_lines_below_previous_blocks) > 0 and chunk_lines[i].strip() == original_lines_below_previous_blocks[0].strip()): + repeating_file_suffix = True + break + + # If none of the above, insert the line! + if False: + await handle_generated_line(chunk_lines[i]) + + lines.append(chunk_lines[i]) + completion_lines_covered += 1 + current_line_in_file += 1 + + await sendDiffUpdate(lines + [common_whitespace if unfinished_line.startswith("<") else (common_whitespace + unfinished_line)], sdk) + finally: + await generator.aclose() + # Add the unfinished line + if unfinished_line != "" and not self.line_to_be_ignored(unfinished_line, completion_lines_covered == 0) and not self.is_end_line(unfinished_line): + unfinished_line = common_whitespace + unfinished_line + lines.append(unfinished_line) + await handle_generated_line(unfinished_line) + completion_lines_covered += 1 + current_line_in_file += 1 + + await sendDiffUpdate(lines, sdk, final=True) + + if False: + # If the current block isn't empty, add that suggestion + if len(current_block_lines) > 0: + # We have a chance to back-track here for blank lines that are repeats of the end of the original + # Don't want to have the same ending in both the original and the generated, can just leave it there + num_to_remove = 0 + for i in range(-1, -len(current_block_lines) - 1, -1): + if len(original_lines_below_previous_blocks) == 0: + break + if current_block_lines[i] == original_lines_below_previous_blocks[-1]: + num_to_remove += 1 + original_lines_below_previous_blocks.pop() + else: + break + current_block_lines = current_block_lines[:- + num_to_remove] if num_to_remove > 0 else current_block_lines + + # It's also possible that some lines match at the beginning of the block + # while len(current_block_lines) > 0 and len(original_lines_below_previous_blocks) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[0]: + # current_block_lines.pop(0) + # original_lines_below_previous_blocks.pop(0) + # current_block_start += 1 + + await sdk.ide.showSuggestion(FileEdit( + filepath=rif.filepath, + range=Range.from_shorthand( + current_block_start, 0, current_block_start + len(original_lines_below_previous_blocks), 0), + replacement="\n".join(current_block_lines) + )) + + # Record the completion + completion = "\n".join(lines) + self._previous_contents = "\n".join(original_lines) + self._new_contents = completion + self._prompt_and_completion += prompt + completion + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + await sdk.update_ui() + + rif_with_contents = [] + for range_in_file in map(lambda x: RangeInFile( + filepath=x.filepath, + # Only consider the range line-by-line. Maybe later don't if it's only a single line. + range=x.range.to_full_lines() + ), self.range_in_files): + file_contents = await sdk.ide.readRangeInFile(range_in_file) + rif_with_contents.append( + RangeInFileWithContents.from_range_in_file(range_in_file, file_contents)) + + rif_dict = {} + for rif in rif_with_contents: + rif_dict[rif.filepath] = rif.contents + + for rif in rif_with_contents: + # If the file doesn't exist, ask them to save it first + if not os.path.exists(rif.filepath): + message = f"The file {rif.filepath} does not exist. Please save it first." + raise ContinueCustomException( + title=message, message=message + ) + + await sdk.ide.setFileOpen(rif.filepath) + await sdk.ide.setSuggestionsLocked(rif.filepath, True) + await self.stream_rif(rif, sdk) + await sdk.ide.setSuggestionsLocked(rif.filepath, False) + + +class EditFileStep(Step): + filepath: str + prompt: str + hide: bool = True + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "Editing file: " + self.filepath + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + file_contents = await sdk.ide.readFile(self.filepath) + await sdk.run_step(DefaultModelEditCodeStep( + range_in_files=[RangeInFile.from_entire_file( + self.filepath, file_contents)], + user_input=self.prompt + )) + + +class ManualEditStep(ReversibleStep): + edit_diff: EditDiff + hide: bool = True + + hide: bool = True + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "Manual edit step" + # TODO - only handling FileEdit here, but need all other types of FileSystemEdits + # Also requires the merge_file_edit function + # return llm.complete(dedent(f"""This code was replaced: + + # {self.edit_diff.backward.replacement} + + # With this code: + + # {self.edit_diff.forward.replacement} + + # Maximally concise summary of changes in bullet points (can use markdown): + # """)) + + @classmethod + def from_sequence(cls, edits: List[FileEditWithFullContents]) -> "ManualEditStep": + diffs = [] + for edit in edits: + _, diff = FileSystem.apply_edit_to_str( + edit.fileContents, edit.fileEdit) + diffs.append(diff) + return cls(edit_diff=EditDiff.from_sequence(diffs)) + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + return None + + async def reverse(self, sdk: ContinueSDK): + await sdk.ide.applyFileSystemEdit(self.edit_diff.backward) + + +class UserInputStep(Step): + user_input: str + name: str = "User Input" + hide: bool = False + + manage_own_chat_context: bool = True + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return self.user_input + + async def run(self, sdk: ContinueSDK) -> Coroutine[UserInputObservation, None, None]: + self.chat_context.append(ChatMessage( + role="user", + content=self.user_input, + summary=self.user_input + )) + return UserInputObservation(user_input=self.user_input) + + +class WaitForUserInputStep(Step): + prompt: str + name: str = "Waiting for user input" + + _description: Union[str, None] = None + _response: Union[str, None] = None + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + if self._response is None: + return self.prompt + else: + return f"{self.prompt}\n\n`{self._response}`" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + self.description = self.prompt + resp = await sdk.wait_for_user_input() + self.description = f"{self.prompt}\n\n`{resp}`" + return TextObservation(text=resp) + + +class WaitForUserConfirmationStep(Step): + prompt: str + name: str = "Waiting for user confirmation" + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return self.prompt + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + self.description = self.prompt + resp = await sdk.wait_for_user_input() + return TextObservation(text=resp) diff --git a/continuedev/src/continuedev/plugins/steps/custom_command.py b/continuedev/src/continuedev/plugins/steps/custom_command.py new file mode 100644 index 00000000..d5b6e48b --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/custom_command.py @@ -0,0 +1,28 @@ +from ...libs.util.templating import render_templated_string +from ...core.main import Step +from ...core.sdk import ContinueSDK +from ..steps.chat import SimpleChatStep + + +class CustomCommandStep(Step): + name: str + prompt: str + user_input: str + slash_command: str + hide: bool = True + + async def describe(self): + return self.prompt + + async def run(self, sdk: ContinueSDK): + task = render_templated_string(self.prompt) + + prompt_user_input = f"Task: {task}. Additional info: {self.user_input}" + messages = await sdk.get_chat_context() + # Find the last chat message with this slash command and replace it with the user input + for i in range(len(messages) - 1, -1, -1): + if messages[i].role == "user" and messages[i].content.startswith(self.slash_command): + messages[i] = messages[i].copy( + update={"content": prompt_user_input}) + break + await sdk.run_step(SimpleChatStep(messages=messages)) diff --git a/continuedev/src/continuedev/plugins/steps/draft/abstract_method.py b/continuedev/src/continuedev/plugins/steps/draft/abstract_method.py new file mode 100644 index 00000000..f3131c4b --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/draft/abstract_method.py @@ -0,0 +1,19 @@ +from ....core.sdk import ContinueSDK +from ....core.main import Step + + +class ImplementAbstractMethodStep(Step): + name: str = "Implement abstract method for all subclasses" + method_name: str + class_name: str + + async def run(self, sdk: ContinueSDK): + + implementations = await sdk.lsp.go_to_implementations(self.class_name) + + for implementation in implementations: + + await sdk.edit_file( + range_in_files=[implementation.range_in_file], + prompt=f"Implement method `{self.method_name}` for this subclass of `{self.class_name}`", + ) diff --git a/continuedev/src/continuedev/plugins/steps/draft/migration.py b/continuedev/src/continuedev/plugins/steps/draft/migration.py new file mode 100644 index 00000000..a76d491b --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/draft/migration.py @@ -0,0 +1,30 @@ +# When an edit is made to an existing class or a new sqlalchemy class is created, +# this should be kicked off. + +from ....core.main import Step + + +class MigrationStep(Step): + name: str = "Create and run an alembic migration." + + edited_file: str + + async def run(self, sdk): + recent_edits = await sdk.ide.get_recent_edits(self.edited_file) + recent_edits_string = "\n\n".join( + map(lambda x: x.to_string(), recent_edits)) + description = await sdk.models.gpt35.complete(f"{recent_edits_string}\n\nGenerate a short description of the migration made in the above changes:\n") + await sdk.run([ + "cd libs", + "poetry run alembic revision --autogenerate -m " + description, + ]) + migration_file = f"libs/alembic/versions/{?}.py" + contents = await sdk.ide.readFile(migration_file) + await sdk.run_step(EditCodeStep( + range_in_files=[RangeInFile.from_entire_file(migration_file, contents)], + prompt=f"Here are the changes made to the sqlalchemy classes:\n\n{recent_edits_string}\n\nThis is the generated migration file:\n\n{{code}}\n\nReview the migration file to make sure it correctly reflects the changes made to the sqlalchemy classes.", + )) + await sdk.run([ + "cd libs", + "poetry run alembic upgrade head", + ]) diff --git a/continuedev/src/continuedev/plugins/steps/draft/redux.py b/continuedev/src/continuedev/plugins/steps/draft/redux.py new file mode 100644 index 00000000..30c8fdbb --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/draft/redux.py @@ -0,0 +1,47 @@ +from ....core.main import Step +from ....core.sdk import ContinueSDK +from ..core.core import EditFileStep + + +class EditReduxStateStep(Step): + + description: str # e.g. "I want to load data from the weatherapi.com API" + + async def run(self, sdk: ContinueSDK): + # Find the right file to edit + + # RootStore + store_filename = "" + sdk.run_step( + EditFileStep( + filename=store_filename, + prompt=f"Edit the root store to add a new slice for {self.description}" + ) + ) + store_file_contents = await sdk.ide.readFile(store_filename) + + # Selector + selector_filename = "" + sdk.run_step(EditFileStep( + filepath=selector_filename, + prompt=f"Edit the selector to add a new property for {self.description}. The store looks like this: {store_file_contents}" + )) + + # Reducer + reducer_filename = "" + sdk.run_step(EditFileStep( + filepath=reducer_filename, + prompt=f"Edit the reducer to add a new property for {self.description}. The store looks like this: {store_file_contents}" + )) + """ + Starts with implementing selector + 1. RootStore + 2. Selector + 3. Reducer or entire slice + + Need to first determine whether this is an: + 1. edit + 2. add new reducer and property in existing slice + 3. add whole new slice + 4. build redux from scratch + """ diff --git a/continuedev/src/continuedev/plugins/steps/draft/typeorm.py b/continuedev/src/continuedev/plugins/steps/draft/typeorm.py new file mode 100644 index 00000000..d06a6fb4 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/draft/typeorm.py @@ -0,0 +1,43 @@ +from textwrap import dedent +from ....core.main import Step +from ....core.sdk import ContinueSDK + + +class CreateTableStep(Step): + sql_str: str + name: str = "Create a table in TypeORM" + + async def run(self, sdk: ContinueSDK): + # Write TypeORM entity + entity_name = self.sql_str.split(" ")[2].capitalize() + await sdk.edit_file( + f"src/entity/{entity_name}.ts", + dedent(f"""\ + {self.sql_str} + + Write a TypeORM entity called {entity_name} for this table, importing as necessary:""") + ) + + # Add entity to data-source.ts + await sdk.edit_file(filepath="src/data-source.ts", prompt=f"Add the {entity_name} entity:") + + # Generate blank migration for the entity + out = await sdk.run(f"npx typeorm migration:create ./src/migration/Create{entity_name}Table") + migration_filepath = out.text.split(" ")[1] + + # Wait for user input + await sdk.wait_for_user_confirmation("Fill in the migration?") + + # Fill in the migration + await sdk.edit_file( + migration_filepath, + dedent(f"""\ + This is the table that was created: + + {self.sql_str} + + Fill in the migration for the table:"""), + ) + + # Run the migration + await sdk.run("npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts") diff --git a/continuedev/src/continuedev/plugins/steps/feedback.py b/continuedev/src/continuedev/plugins/steps/feedback.py new file mode 100644 index 00000000..119e3112 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/feedback.py @@ -0,0 +1,17 @@ +from typing import Coroutine +from ...core.main import Models +from ...core.main import Step +from ...core.sdk import ContinueSDK +from ...libs.util.telemetry import capture_event + + +class FeedbackStep(Step): + user_input: str + name = "Thanks for your feedback!" + + async def describe(self, models: Models): + return f"`{self.user_input}`\n\nWe'll see your feedback and make improvements as soon as possible. If you'd like to directly email us, you can contact [nate@continue.dev](mailto:nate@continue.dev?subject=Feedback%20On%20Continue)." + + async def run(self, sdk: ContinueSDK): + capture_event(sdk.ide.unique_id, "feedback", + {"feedback": self.user_input}) diff --git a/continuedev/src/continuedev/plugins/steps/find_and_replace.py b/continuedev/src/continuedev/plugins/steps/find_and_replace.py new file mode 100644 index 00000000..a2c9c44e --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/find_and_replace.py @@ -0,0 +1,28 @@ +from ...models.filesystem_edit import FileEdit, Range +from ...core.main import Models, Step +from ...core.sdk import ContinueSDK + + +class FindAndReplaceStep(Step): + name: str = "Find and replace" + filepath: str + pattern: str + replacement: str + + async def describe(self, models: Models): + return f"Replaced all instances of `{self.pattern}` with `{self.replacement}` in `{self.filepath}`" + + async def run(self, sdk: ContinueSDK): + file_content = await sdk.ide.readFile(self.filepath) + while self.pattern in file_content: + start_index = file_content.index(self.pattern) + end_index = start_index + len(self.pattern) + await sdk.ide.applyFileSystemEdit(FileEdit( + filepath=self.filepath, + range=Range.from_indices( + file_content, start_index, end_index - 1), + replacement=self.replacement + )) + file_content = file_content[:start_index] + \ + self.replacement + file_content[end_index:] + await sdk.ide.saveFile(self.filepath) diff --git a/continuedev/src/continuedev/plugins/steps/help.py b/continuedev/src/continuedev/plugins/steps/help.py new file mode 100644 index 00000000..5111c7cf --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/help.py @@ -0,0 +1,59 @@ +from textwrap import dedent +from ...core.main import ChatMessage, Step +from ...core.sdk import ContinueSDK +from ...libs.util.telemetry import capture_event + +help = dedent("""\ + Continue is an open-source coding autopilot. It is a VS Code extension that brings the power of ChatGPT to your IDE. + + It gathers context for you and stores your interactions automatically, so that you can avoid copy/paste now and benefit from a customized Large Language Model (LLM) later. + + Continue can be used to... + 1. Edit chunks of code with specific instructions (e.g. "/edit migrate this digital ocean terraform file into one that works for GCP") + 2. Get answers to questions without switching windows (e.g. "how do I find running process on port 8000?") + 3. Generate files from scratch (e.g. "/edit Create a Python CLI tool that uses the posthog api to get events from DAUs") + + You tell Continue to edit a specific section of code by highlighting it. If you highlight multiple code sections, then it will only edit the one with the purple glow around it. You can switch which one has the purple glow by clicking the paint brush. + + If you don't highlight any code, then Continue will insert at the location of your cursor. + + Continue passes all of the sections of code you highlight, the code above and below the to-be edited highlighted code section, and all previous steps above input box as context to the LLM. + + You can use cmd+m (Mac) / ctrl+m (Windows) to open Continue. You can use cmd+shift+e / ctrl+shift+e to open file Explorer. You can add your own OpenAI API key to VS Code Settings with `cmd+,` + + If Continue is stuck loading, try using `cmd+shift+p` to open the command palette, search "Reload Window", and then select it. This will reload VS Code and Continue and often fixes issues. + + If you have feedback, please use /feedback to let us know how you would like to use Continue. We are excited to hear from you!""") + + +class HelpStep(Step): + + name: str = "Help" + user_input: str + manage_own_chat_context: bool = True + description: str = "" + + async def run(self, sdk: ContinueSDK): + + question = self.user_input + + prompt = dedent(f"""Please us the information below to provide a succinct answer to the following quesiton: {question} + + Information: + + {help}""") + + self.chat_context.append(ChatMessage( + role="user", + content=prompt, + summary="Help" + )) + messages = await sdk.get_chat_context() + generator = sdk.models.gpt4.stream_chat(messages) + async for chunk in generator: + if "content" in chunk: + self.description += chunk["content"] + await sdk.update_ui() + + capture_event(sdk.ide.unique_id, "help", { + "question": question, "answer": self.description}) diff --git a/continuedev/src/continuedev/plugins/steps/input/nl_multiselect.py b/continuedev/src/continuedev/plugins/steps/input/nl_multiselect.py new file mode 100644 index 00000000..b54d394a --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/input/nl_multiselect.py @@ -0,0 +1,28 @@ +from typing import List, Union +from ..core.core import WaitForUserInputStep +from ....core.main import Step +from ....core.sdk import ContinueSDK + + +class NLMultiselectStep(Step): + hide: bool = True + + prompt: str + options: List[str] + + async def run(self, sdk: ContinueSDK): + user_response = (await sdk.run_step(WaitForUserInputStep(prompt=self.prompt))).text + + def extract_option(text: str) -> Union[str, None]: + for option in self.options: + if option in text: + return option + return None + + first_try = extract_option(user_response.lower()) + if first_try is not None: + return first_try + + gpt_parsed = await sdk.models.gpt35.complete( + f"These are the available options are: [{', '.join(self.options)}]. The user requested {user_response}. This is the exact string from the options array that they selected:") + return extract_option(gpt_parsed) or self.options[0] diff --git a/continuedev/src/continuedev/plugins/steps/main.py b/continuedev/src/continuedev/plugins/steps/main.py new file mode 100644 index 00000000..30117c55 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/main.py @@ -0,0 +1,314 @@ +import os +from typing import Coroutine, List, Union +from textwrap import dedent +from pydantic import BaseModel, Field + +from ...models.main import Traceback, Range +from ...models.filesystem_edit import EditDiff, FileEdit +from ...models.filesystem import RangeInFile, RangeInFileWithContents +from ...core.observation import Observation +from ...libs.llm.prompt_utils import MarkdownStyleEncoderDecoder +from ...core.main import ContinueCustomException, Step +from ...core.sdk import ContinueSDK, Models +from ...core.observation import Observation +from .core.core import DefaultModelEditCodeStep +from ...libs.util.calculate_diff import calculate_diff2 + + +class SetupContinueWorkspaceStep(Step): + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "Set up Continue workspace by adding a .continue directory" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + if not os.path.exists(os.path.join(await sdk.ide.getWorkspaceDirectory(), ".continue")): + await sdk.add_directory(".continue") + if not os.path.exists(os.path.join(await sdk.ide.getWorkspaceDirectory(), ".continue", "config.json")): + await sdk.add_file(".continue/config.json", dedent("""\ + { + "allow_anonymous_telemetry": true + }""")) + + +class Policy(BaseModel): + pass + + +class RunPolicyUntilDoneStep(Step): + policy: "Policy" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + next_step = self.policy.next(sdk.config, sdk.history) + while next_step is not None: + observation = await sdk.run_step(next_step) + next_step = self.policy.next(sdk.config, sdk.history) + return observation + + +class FasterEditHighlightedCodeStep(Step): + user_input: str + hide = True + _completion: str = "Edit Code" + _edit_diffs: Union[List[EditDiff], None] = None + _prompt: str = dedent("""\ + You will be given code to edit in order to perfectly satisfy the user request. All the changes you make must be described as replacements, which you should format in the following way: + FILEPATH + + REPLACE_ME + + REPLACE_WITH + + + where and can be multiple lines, but should be the mininum needed to make the edit. Be sure to maintain existing whitespace at the start of lines. + + For example, if you want to replace the code `x = 1` with `x = 2` in main.py, you would write: + FILEPATH + main.py + REPLACE_ME + x = 1 + REPLACE_WITH + x = 2 + If you wanted to delete the code + ``` + def sum(a, b): + return a + b + ``` + in main.py, you would write: + FILEPATH + main.py + REPLACE_ME + def sum(a, b): + return a + b + REPLACE_WITH + + You may need to make multiple edits; respond with exactly as many as needed. + + Below is the code before changes: + + {code} + + This is the user request: "{user_input}" + Here is the description of changes to make: +""") + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "Editing highlighted code" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + range_in_files = await sdk.get_code_context(only_editing=True) + if len(range_in_files) == 0: + # Get the full contents of all visible files + files = await sdk.ide.getVisibleFiles() + contents = {} + for file in files: + contents[file] = await sdk.ide.readFile(file) + + range_in_files = [RangeInFileWithContents.from_entire_file( + filepath, content) for filepath, content in contents.items()] + + enc_dec = MarkdownStyleEncoderDecoder(range_in_files) + code_string = enc_dec.encode() + prompt = self._prompt.format( + code=code_string, user_input=self.user_input) + + rif_dict = {} + for rif in range_in_files: + rif_dict[rif.filepath] = rif.contents + + completion = await sdk.models.gpt35.complete(prompt) + + # Temporarily doing this to generate description. + self._prompt = prompt + self._completion = completion + print(completion) + + # ALTERNATIVE DECODING STEP HERE + raw_file_edits = [] + lines = completion.split("\n") + current_edit = {} + status = "FILEPATH" + for i in range(0, len(lines)): + line = lines[i] + if line == "FILEPATH": + if "FILEPATH" in current_edit: + raw_file_edits.append(current_edit) + current_edit = {} + status = "FILEPATH" + elif line == "REPLACE_ME": + status = "REPLACE_ME" + elif line == "REPLACE_WITH": + status = "REPLACE_WITH" + elif status == "FILEPATH": + current_edit["filepath"] = line + elif status == "REPLACE_ME": + if "replace_me" in current_edit: + current_edit["replace_me"] += "\n" + line + else: + current_edit["replace_me"] = line + elif status == "REPLACE_WITH": + if "replace_with" in current_edit: + current_edit["replace_with"] += "\n" + line + else: + current_edit["replace_with"] = line + if "filepath" in current_edit: + raw_file_edits.append(current_edit) + + file_edits = [] + for edit in raw_file_edits: + filepath = edit["filepath"] + replace_me = edit["replace_me"] + replace_with = edit["replace_with"] + file_edits.append( + FileEdit(filepath=filepath, range=Range.from_lines_snippet_in_file(content=rif_dict[filepath], snippet=replace_me), replacement=replace_with)) + # ------------------------------ + + self._edit_diffs = [] + for file_edit in file_edits: + diff = await sdk.apply_filesystem_edit(file_edit) + self._edit_diffs.append(diff) + + for filepath in set([file_edit.filepath for file_edit in file_edits]): + await sdk.ide.saveFile(filepath) + await sdk.ide.setFileOpen(filepath) + + return None + + +class StarCoderEditHighlightedCodeStep(Step): + user_input: str + name: str = "Editing Code" + hide = False + _prompt: str = "{code}{user_request}" + + _prompt_and_completion: str = "" + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return await models.gpt35.complete(f"{self._prompt_and_completion}\n\nPlease give brief a description of the changes made above using markdown bullet points:") + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + range_in_files = await sdk.get_code_context(only_editing=True) + found_highlighted_code = len(range_in_files) > 0 + if not found_highlighted_code: + # Get the full contents of all visible files + files = await sdk.ide.getVisibleFiles() + contents = {} + for file in files: + contents[file] = await sdk.ide.readFile(file) + + range_in_files = [RangeInFileWithContents.from_entire_file( + filepath, content) for filepath, content in contents.items()] + + rif_dict = {} + for rif in range_in_files: + rif_dict[rif.filepath] = rif.contents + + for rif in range_in_files: + prompt = self._prompt.format( + code=rif.contents, user_request=self.user_input) + + if found_highlighted_code: + full_file_contents = await sdk.ide.readFile(rif.filepath) + segs = full_file_contents.split(rif.contents) + prompt = f"{segs[0]}{segs[1]}" + prompt + + completion = str(await sdk.models.starcoder.complete(prompt)) + eot_token = "<|endoftext|>" + completion = completion.removesuffix(eot_token) + + if found_highlighted_code: + rif.contents = segs[0] + rif.contents + segs[1] + completion = segs[0] + completion + segs[1] + + self._prompt_and_completion += prompt + completion + + edits = calculate_diff2( + rif.filepath, rif.contents, completion.removesuffix("\n")) + for edit in edits: + await sdk.ide.applyFileSystemEdit(edit) + + # await sdk.ide.applyFileSystemEdit( + # FileEdit(filepath=rif.filepath, range=rif.range, replacement=completion)) + await sdk.ide.saveFile(rif.filepath) + await sdk.ide.setFileOpen(rif.filepath) + + +class EditHighlightedCodeStep(Step): + user_input: str = Field( + ..., title="User Input", description="The natural language request describing how to edit the code") + hide = True + description: str = "Change the contents of the currently highlighted code or open file. You should call this function if the user asks seems to be asking for a code change." + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "Editing code" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + range_in_files = sdk.get_code_context(only_editing=True) + + # If nothing highlighted, insert at the cursor if possible + if len(range_in_files) == 0: + highlighted_code = await sdk.ide.getHighlightedCode() + if highlighted_code is not None: + for rif in highlighted_code: + if os.path.dirname(rif.filepath) == os.path.expanduser(os.path.join("~", ".continue", "diffs")): + raise ContinueCustomException( + message="Please accept or reject the change before making another edit in this file.", title="Accept/Reject First") + if rif.range.start == rif.range.end: + range_in_files.append( + RangeInFileWithContents.from_range_in_file(rif, "")) + + # If still no highlighted code, raise error + if len(range_in_files) == 0: + raise ContinueCustomException( + message="Please highlight some code and try again.", title="No Code Selected") + + range_in_files = list(map(lambda x: RangeInFile( + filepath=x.filepath, range=x.range + ), range_in_files)) + + for range_in_file in range_in_files: + if os.path.dirname(range_in_file.filepath) == os.path.expanduser(os.path.join("~", ".continue", "diffs")): + self.description = "Please accept or reject the change before making another edit in this file." + return + + await sdk.run_step(DefaultModelEditCodeStep(user_input=self.user_input, range_in_files=range_in_files)) + + +class UserInputStep(Step): + user_input: str + + +class SolveTracebackStep(Step): + traceback: Traceback + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return f"```\n{self.traceback.full_traceback}\n```" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + prompt = dedent("""I ran into this problem with my Python code: + + {traceback} + + Below are the files that might need to be fixed: + + {code} + + This is what the code should be in order to avoid the problem: + """).format(traceback=self.traceback.full_traceback, code="{code}") + + range_in_files = [] + for frame in self.traceback.frames: + content = await sdk.ide.readFile(frame.filepath) + range_in_files.append( + RangeInFile.from_entire_file(frame.filepath, content)) + + await sdk.run_step(DefaultModelEditCodeStep(range_in_files=range_in_files, user_input=prompt)) + return None + + +class EmptyStep(Step): + hide: bool = True + + async def describe(self, models: Models) -> Coroutine[str, None, None]: + return "" + + async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: + pass diff --git a/continuedev/src/continuedev/plugins/steps/on_traceback.py b/continuedev/src/continuedev/plugins/steps/on_traceback.py new file mode 100644 index 00000000..e99f212d --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/on_traceback.py @@ -0,0 +1,27 @@ +import os + +from .core.core import UserInputStep +from ...core.main import ChatMessage, Step +from ...core.sdk import ContinueSDK +from .chat import SimpleChatStep + + +class DefaultOnTracebackStep(Step): + output: str + name: str = "Help With Traceback" + hide: bool = True + + async def run(self, sdk: ContinueSDK): + # Add context for any files in the traceback that are in the workspace + for line in self.output.split("\n"): + segs = line.split(" ") + for seg in segs: + if seg.startswith(os.path.sep) and os.path.exists(seg) and os.path.commonprefix([seg, sdk.ide.workspace_directory]) == sdk.ide.workspace_directory: + file_contents = await sdk.ide.readFile(seg) + self.chat_context.append(ChatMessage( + role="user", + content=f"The contents of {seg}:\n```\n{file_contents}\n```", + summary="" + )) + await sdk.run_step(UserInputStep(user_input=f"""I got the following error, can you please help explain how to fix it?\n\n{self.output}""")) + await sdk.run_step(SimpleChatStep(name="Help With Traceback")) diff --git a/continuedev/src/continuedev/plugins/steps/open_config.py b/continuedev/src/continuedev/plugins/steps/open_config.py new file mode 100644 index 00000000..d950c26f --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/open_config.py @@ -0,0 +1,29 @@ +from textwrap import dedent +from ...core.main import Step +from ...core.sdk import ContinueSDK +import os + + +class OpenConfigStep(Step): + name: str = "Open config" + + async def describe(self, models): + return dedent("""\ + `\"config.json\"` is now open. You can add a custom slash command in the `\"custom_commands\"` section, like in this example: + ```json + "custom_commands": [ + { + "name": "test", + "description": "Write unit tests like I do for the highlighted code", + "prompt": "Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated." + } + ] + ``` + `"name"` is the command you will type. + `"description"` is the description displayed in the slash command menu. + `"prompt"` is the instruction given to the model. The overall prompt becomes "Task: {prompt}, Additional info: {user_input}". For example, if you entered "/test exactly 5 assertions", the overall prompt would become "Task: Write a comprehensive...and sophisticated, Additional info: exactly 5 assertions".""") + + async def run(self, sdk: ContinueSDK): + global_dir = os.path.expanduser('~/.continue') + config_path = os.path.join(global_dir, 'config.json') + await sdk.ide.setFileOpen(config_path) diff --git a/continuedev/src/continuedev/plugins/steps/react.py b/continuedev/src/continuedev/plugins/steps/react.py new file mode 100644 index 00000000..8b2e7c2e --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/react.py @@ -0,0 +1,42 @@ +from textwrap import dedent +from typing import List, Union, Tuple +from ...core.main import Step +from ...core.sdk import ContinueSDK + + +class NLDecisionStep(Step): + user_input: str + default_step: Union[Step, None] = None + steps: List[Tuple[Step, str]] + + hide: bool = False + name: str = "Deciding what to do next" + + async def run(self, sdk: ContinueSDK): + step_descriptions = "\n".join([ + f"- {step[0].name}: {step[1]}" + for step in self.steps + ]) + prompt = dedent(f"""\ + The following steps are available, in the format "- [step name]: [step description]": + {step_descriptions} + + The user gave the following input: + + {self.user_input} + + Select the step which should be taken next to satisfy the user input. Say only the name of the selected step. You must choose one:""") + + resp = (await sdk.models.gpt35.complete(prompt)).lower() + + step_to_run = None + for step in self.steps: + if step[0].name.lower() in resp: + step_to_run = step[0] + + step_to_run = step_to_run or self.default_step or self.steps[0] + + self.hide = True + await sdk.update_ui() + + await sdk.run_step(step_to_run) diff --git a/continuedev/src/continuedev/plugins/steps/search_directory.py b/continuedev/src/continuedev/plugins/steps/search_directory.py new file mode 100644 index 00000000..7d02d6fa --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/search_directory.py @@ -0,0 +1,69 @@ +import asyncio +from textwrap import dedent +from typing import List, Union + +from ...models.filesystem import RangeInFile +from ...models.main import Range +from ...core.main import Step +from ...core.sdk import ContinueSDK +from ...libs.util.create_async_task import create_async_task +import os +import re + +# Already have some code for this somewhere +IGNORE_DIRS = ["env", "venv", ".venv"] +IGNORE_FILES = [".env"] + + +def find_all_matches_in_dir(pattern: str, dirpath: str) -> List[RangeInFile]: + range_in_files = [] + for root, dirs, files in os.walk(dirpath): + dirname = os.path.basename(root) + if dirname.startswith(".") or dirname in IGNORE_DIRS: + continue + for file in files: + if file in IGNORE_FILES: + continue + with open(os.path.join(root, file), "r") as f: + # Find the index of all occurences of the pattern in the file. Use re. + file_content = f.read() + results = re.finditer(pattern, file_content) + range_in_files += [ + RangeInFile(filepath=os.path.join(root, file), range=Range.from_indices( + file_content, result.start(), result.end())) + for result in results + ] + + return range_in_files + + +class WriteRegexPatternStep(Step): + user_request: str + + async def run(self, sdk: ContinueSDK): + # Ask the user for a regex pattern + pattern = await sdk.models.gpt35.complete(dedent(f"""\ + This is the user request: + + {self.user_request} + + Please write either a regex pattern or just a string that be used with python's re module to find all matches requested by the user. It will be used as `re.findall(, file_content)`. Your output should be only the regex or string, nothing else:""")) + + return pattern + + +class EditAllMatchesStep(Step): + pattern: str + user_request: str + directory: Union[str, None] = None + + async def run(self, sdk: ContinueSDK): + # Search all files for a given string + range_in_files = find_all_matches_in_dir(self.pattern, self.directory or await sdk.ide.getWorkspaceDirectory()) + + tasks = [create_async_task(sdk.edit_file( + range=range_in_file.range, + filename=range_in_file.filepath, + prompt=self.user_request + ), sdk.ide.unique_id) for range_in_file in range_in_files] + await asyncio.gather(*tasks) diff --git a/continuedev/src/continuedev/plugins/steps/steps_on_startup.py b/continuedev/src/continuedev/plugins/steps/steps_on_startup.py new file mode 100644 index 00000000..19d62d30 --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/steps_on_startup.py @@ -0,0 +1,17 @@ +from ...core.main import Step +from ...core.sdk import Models, ContinueSDK +from ...libs.util.step_name_to_steps import get_step_from_name + + +class StepsOnStartupStep(Step): + hide: bool = True + + async def describe(self, models: Models): + return "Running steps on startup" + + async def run(self, sdk: ContinueSDK): + steps_on_startup = sdk.config.steps_on_startup + + for step_name, step_params in steps_on_startup.items(): + step = get_step_from_name(step_name, step_params) + await sdk.run_step(step) diff --git a/continuedev/src/continuedev/plugins/steps/welcome.py b/continuedev/src/continuedev/plugins/steps/welcome.py new file mode 100644 index 00000000..df3e9a8a --- /dev/null +++ b/continuedev/src/continuedev/plugins/steps/welcome.py @@ -0,0 +1,33 @@ +from textwrap import dedent +import os + +from ...models.filesystem_edit import AddFile +from ...core.main import Step +from ...core.sdk import ContinueSDK, Models + + +class WelcomeStep(Step): + name: str = "Welcome to Continue!" + hide: bool = True + + async def describe(self, models: Models): + return "Welcome to Continue!" + + async def run(self, sdk: ContinueSDK): + continue_dir = os.path.expanduser("~/.continue") + filepath = os.path.join(continue_dir, "calculator.py") + if os.path.exists(filepath): + return + if not os.path.exists(continue_dir): + os.mkdir(continue_dir) + + await sdk.ide.applyFileSystemEdit(AddFile(filepath=filepath, content=dedent("""\ + \"\"\" + Welcome to Continue! To learn how to use it, delete this comment and try to use Continue for the following: + - "Write me a calculator class" + - Ask for a new method (e.g. "exp", "mod", "sqrt") + - Type /comment to write comments for the entire class + - Ask about how the class works, how to write it in another language, etc. + \"\"\""""))) + + # await sdk.ide.setFileOpen(filepath=filepath) diff --git a/continuedev/src/continuedev/recipes/AddTransformRecipe/README.md b/continuedev/src/continuedev/recipes/AddTransformRecipe/README.md deleted file mode 100644 index d735e0cd..00000000 --- a/continuedev/src/continuedev/recipes/AddTransformRecipe/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# AddTransformRecipe - -Uses the Chess.com API example to show how to add map and filter Python transforms to a dlt pipeline. - -Background -- https://dlthub.com/docs/general-usage/resource#filter-transform-and-pivot-data -- https://dlthub.com/docs/customizations/customizing-pipelines/renaming_columns -- https://dlthub.com/docs/customizations/customizing-pipelines/pseudonymizing_columns \ No newline at end of file diff --git a/continuedev/src/continuedev/recipes/AddTransformRecipe/dlt_transform_docs.md b/continuedev/src/continuedev/recipes/AddTransformRecipe/dlt_transform_docs.md deleted file mode 100644 index 658b285f..00000000 --- a/continuedev/src/continuedev/recipes/AddTransformRecipe/dlt_transform_docs.md +++ /dev/null @@ -1,135 +0,0 @@ -# Customize resources -## Filter, transform and pivot data - -You can attach any number of transformations that are evaluated on item per item basis to your resource. The available transformation types: -- map - transform the data item (resource.add_map) -- filter - filter the data item (resource.add_filter) -- yield map - a map that returns iterator (so single row may generate many rows - resource.add_yield_map) - -Example: We have a resource that loads a list of users from an api endpoint. We want to customize it so: -- we remove users with user_id == 'me' -- we anonymize user data -Here's our resource: -```python -import dlt - -@dlt.resource(write_disposition='replace') -def users(): - ... - users = requests.get(...) - ... - yield users -``` - -Here's our script that defines transformations and loads the data. -```python -from pipedrive import users - -def anonymize_user(user_data): - user_data['user_id'] = hash_str(user_data['user_id']) - user_data['user_email'] = hash_str(user_data['user_email']) - return user_data - -# add the filter and anonymize function to users resource and enumerate -for user in users().add_filter(lambda user: user['user_id'] != 'me').add_map(anonymize_user): -print(user) -``` - -Here is a more complex example of a filter transformation: - - # Renaming columns - ## Renaming columns by replacing the special characters - - In the example below, we create a dummy source with special characters in the name. We then write a function that we intend to apply to the resource to modify its output (i.e. replacing the German umlaut): replace_umlauts_in_dict_keys. - ```python - import dlt - - # create a dummy source with umlauts (special characters) in key names (um) - @dlt.source - def dummy_source(prefix: str = None): - @dlt.resource - def dummy_data(): - for _ in range(100): - yield {f'Objekt_{_}':{'Größe':_, 'Äquivalenzprüfung':True}} - return dummy_data(), - - def replace_umlauts_in_dict_keys(d): - # Replaces umlauts in dictionary keys with standard characters. - umlaut_map = {'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', 'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue'} - result = {} - for k, v in d.items(): - new_key = ''.join(umlaut_map.get(c, c) for c in k) - if isinstance(v, dict): - result[new_key] = replace_umlauts_in_dict_keys(v) - else: - result[new_key] = v - return result - - # We can add the map function to the resource - - # 1. Create an instance of the source so you can edit it. - data_source = dummy_source() - - # 2. Modify this source instance's resource - data_source = data_source.dummy_data().add_map(replace_umlauts_in_dict_keys) - - # 3. Inspect your result - for row in data_source: - print(row) - - # {'Objekt_0': {'Groesse': 0, 'Aequivalenzpruefung': True}} - # ... - ``` - -Here is a more complex example of a map transformation: - -# Pseudonymizing columns -## Pseudonymizing (or anonymizing) columns by replacing the special characters -Pseudonymization is a deterministic way to hide personally identifiable info (PII), enabling us to consistently achieve the same mapping. If instead you wish to anonymize, you can delete the data, or replace it with a constant. In the example below, we create a dummy source with a PII column called 'name', which we replace with deterministic hashes (i.e. replacing the German umlaut). - -```python -import dlt -import hashlib - -@dlt.source -def dummy_source(prefix: str = None): - @dlt.resource - def dummy_data(): - for _ in range(3): - yield {'id':_, 'name': f'Jane Washington {_}'} - return dummy_data(), - -def pseudonymize_name(doc): - Pseudonmyisation is a deterministic type of PII-obscuring - Its role is to allow identifying users by their hash, without revealing the underlying info. - - # add a constant salt to generate - salt = 'WI@N57%zZrmk#88c' - salted_string = doc['name'] + salt - sh = hashlib.sha256() - sh.update(salted_string.encode()) - hashed_string = sh.digest().hex() - doc['name'] = hashed_string - return doc - - # run it as is - for row in dummy_source().dummy_data().add_map(pseudonymize_name): - print(row) - - #{'id': 0, 'name': '96259edb2b28b48bebce8278c550e99fbdc4a3fac8189e6b90f183ecff01c442'} - #{'id': 1, 'name': '92d3972b625cbd21f28782fb5c89552ce1aa09281892a2ab32aee8feeb3544a1'} - #{'id': 2, 'name': '443679926a7cff506a3b5d5d094dc7734861352b9e0791af5d39db5a7356d11a'} - - # Or create an instance of the data source, modify the resource and run the source. - - # 1. Create an instance of the source so you can edit it. - data_source = dummy_source() - # 2. Modify this source instance's resource - data_source = data_source.dummy_data().add_map(replace_umlauts_in_dict_keys) - # 3. Inspect your result - for row in data_source: - print(row) - - pipeline = dlt.pipeline(pipeline_name='example', destination='bigquery', dataset_name='normalized_data') - load_info = pipeline.run(data_source) -``` \ No newline at end of file diff --git a/continuedev/src/continuedev/recipes/AddTransformRecipe/main.py b/continuedev/src/continuedev/recipes/AddTransformRecipe/main.py deleted file mode 100644 index fdd343f5..00000000 --- a/continuedev/src/continuedev/recipes/AddTransformRecipe/main.py +++ /dev/null @@ -1,27 +0,0 @@ -from textwrap import dedent - -from ...core.main import Step -from ...core.sdk import ContinueSDK -from ...steps.core.core import WaitForUserInputStep -from ...steps.core.core import MessageStep -from .steps import SetUpChessPipelineStep, AddTransformStep - - -class AddTransformRecipe(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - text_observation = await sdk.run_step( - MessageStep(message=dedent("""\ - This recipe will walk you through the process of adding a transform to a dlt pipeline that uses the chess.com API source. With the help of Continue, you will: - - Set up a dlt pipeline for the chess.com API - - Add a filter or map transform to the pipeline - - Run the pipeline and view the transformed data in a Streamlit app"""), name="Add transformation to a dlt pipeline") >> - SetUpChessPipelineStep() >> - WaitForUserInputStep( - prompt="How do you want to transform the Chess.com API data before loading it? For example, you could filter out games that ended in a draw.") - ) - await sdk.run_step( - AddTransformStep( - transform_description=text_observation.text) - ) diff --git a/continuedev/src/continuedev/recipes/AddTransformRecipe/steps.py b/continuedev/src/continuedev/recipes/AddTransformRecipe/steps.py deleted file mode 100644 index 9744146c..00000000 --- a/continuedev/src/continuedev/recipes/AddTransformRecipe/steps.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -from textwrap import dedent - -from ...models.main import Range -from ...models.filesystem import RangeInFile -from ...steps.core.core import MessageStep -from ...core.sdk import Models -from ...core.observation import DictObservation -from ...models.filesystem_edit import AddFile -from ...core.main import Step -from ...core.sdk import ContinueSDK - -AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" - - -class SetUpChessPipelineStep(Step): - hide: bool = True - name: str = "Setup Chess.com API dlt Pipeline" - - async def describe(self, models: Models): - return "This step will create a new dlt pipeline that loads data from the chess.com API." - - async def run(self, sdk: ContinueSDK): - - # running commands to get started when creating a new dlt pipeline - await sdk.run([ - 'python3 -m venv .env', - 'source .env/bin/activate', - 'pip install dlt', - 'dlt --non-interactive init chess duckdb', - 'pip install -r requirements.txt', - 'pip install pandas streamlit' # Needed for the pipeline show step later - ], name="Set up Python environment", description=dedent(f"""\ - - Create a Python virtual environment: `python3 -m venv .env` - - Activate the virtual environment: `source .env/bin/activate` - - Install dlt: `pip install dlt` - - Create a new dlt pipeline called "chess" that loads data into a local DuckDB instance: `dlt init chess duckdb` - - Install the Python dependencies for the pipeline: `pip install -r requirements.txt`""")) - - -class AddTransformStep(Step): - hide: bool = True - - # e.g. "Use the `python-chess` library to decode the moves in the game data" - transform_description: str - - async def run(self, sdk: ContinueSDK): - source_name = 'chess' - filename = f'{source_name}_pipeline.py' - abs_filepath = os.path.join(sdk.ide.workspace_directory, filename) - - # Open the file and highlight the function to be edited - await sdk.ide.setFileOpen(abs_filepath) - - await sdk.run_step(MessageStep(message=dedent("""\ - This step will customize your resource function with a transform of your choice: - - Add a filter or map transformation depending on your request - - Load the data into a local DuckDB instance - - Open up a Streamlit app for you to view the data"""), name="Write transformation function")) - - with open(os.path.join(os.path.dirname(__file__), 'dlt_transform_docs.md')) as f: - dlt_transform_docs = f.read() - - prompt = dedent(f"""\ - Task: Write a transform function using the description below and then use `add_map` or `add_filter` from the `dlt` library to attach it a resource. - - Description: {self.transform_description} - - Here are some docs pages that will help you better understand how to use `dlt`. - - {dlt_transform_docs}""") - - # edit the pipeline to add a tranform function and attach it to a resource - await sdk.edit_file( - filename=filename, - prompt=prompt, - name=f"Writing transform function {AI_ASSISTED_STRING}" - ) - - await sdk.wait_for_user_confirmation("Press Continue to confirm that the changes are okay before we run the pipeline.") - - # run the pipeline and load the data - await sdk.run(f'python3 {filename}', name="Run the pipeline", description=f"Running `python3 {filename}` to load the data into a local DuckDB instance") - - # run a streamlit app to show the data - await sdk.run(f'dlt pipeline {source_name}_pipeline show', name="Show data in a Streamlit app", description=f"Running `dlt pipeline {source_name} show` to show the data in a Streamlit app, where you can view and play with the data.") diff --git a/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/README.md b/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/README.md deleted file mode 100644 index df66104f..00000000 --- a/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# ContinueRecipeRecipe - -A recipe for building recipes! - -## How to use this recipe - -This recipe takes a single input, a description of the recipe to be built. diff --git a/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/main.py b/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/main.py deleted file mode 100644 index 953fb0c2..00000000 --- a/continuedev/src/continuedev/recipes/ContinueRecipeRecipe/main.py +++ /dev/null @@ -1,37 +0,0 @@ -from textwrap import dedent -from ...models.filesystem import RangeInFile -from ...steps.main import EditHighlightedCodeStep -from ...core.main import Step -from ...core.sdk import ContinueSDK - - -class ContinueStepStep(Step): - name: str = "Write your own Continue Step." - prompt: str - - async def run(self, sdk: ContinueSDK): - await sdk.run_step(EditHighlightedCodeStep(user_input=dedent(f"""\ - Here is an example of a Step that runs a command and then edits a file. - - ```python - from ...core.main import Step - from ...core.sdk import ContinueSDK - - class RunCommandAndEditFileStep(Step): - name: str = "Run a command and then edit a file." - command: str - file_path: str - prompt: str - - async def run(self, sdk: ContinueSDK): - await sdk.run([command]) - await sdk.edit_file(filename=self.file_path, prompt=self.prompt) - ``` - - Please edit the code to write your own Step that does the following: - - {self.prommpt} - - It should be a subclass of Step as above, implementing the `run` method, and using pydantic attributes to define the parameters. - - """))) diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/README.md b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py deleted file mode 100644 index 55ef107b..00000000 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py +++ /dev/null @@ -1,33 +0,0 @@ -from textwrap import dedent - -from ...core.sdk import ContinueSDK -from ...core.main import Step -from ...steps.core.core import WaitForUserInputStep -from ...steps.core.core import MessageStep -from .steps import SetupPipelineStep, ValidatePipelineStep, RunQueryStep - - -class CreatePipelineRecipe(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - text_observation = await sdk.run_step( - MessageStep(name="Building your first dlt pipeline", message=dedent("""\ - This recipe will walk you through the process of creating a dlt pipeline for your chosen data source. With the help of Continue, you will: - - Create a Python virtual environment with dlt installed - - Run `dlt init` to generate a pipeline template - - Write the code to call the API - - Add any required API keys to the `secrets.toml` file - - Test that the API call works - - Load the data into a local DuckDB instance - - Write a query to view the data""")) >> - WaitForUserInputStep( - prompt="What API do you want to load data from? (e.g. weatherapi.com, chess.com)") - ) - await sdk.run_step( - SetupPipelineStep(api_description=text_observation.text) >> - ValidatePipelineStep() >> - RunQueryStep() >> - MessageStep( - name="Congrats!", message="You've successfully created your first dlt pipeline! 🎉") - ) diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py deleted file mode 100644 index 60218ef9..00000000 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py +++ /dev/null @@ -1,170 +0,0 @@ -import os -import subprocess -from textwrap import dedent -import time - -from ...models.main import Range -from ...models.filesystem import RangeInFile -from ...steps.core.core import MessageStep -from ...core.observation import DictObservation, InternalErrorObservation -from ...models.filesystem_edit import AddFile, FileEdit -from ...core.main import Step -from ...core.sdk import ContinueSDK, Models - -AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" - - -class SetupPipelineStep(Step): - hide: bool = True - name: str = "Setup dlt Pipeline" - - api_description: str # e.g. "I want to load data from the weatherapi.com API" - - async def describe(self, models: Models): - return dedent(f"""\ - This step will create a new dlt pipeline that loads data from an API, as per your request: - {self.api_description} - """) - - async def run(self, sdk: ContinueSDK): - sdk.context.set("api_description", self.api_description) - - source_name = (await sdk.models.gpt35.complete( - f"Write a snake_case name for the data source described by {self.api_description}: ")).strip() - filename = f'{source_name}.py' - - # running commands to get started when creating a new dlt pipeline - await sdk.run([ - 'python3 -m venv .env', - 'source .env/bin/activate', - 'pip install dlt', - f'dlt --non-interactive init {source_name} duckdb', - 'pip install -r requirements.txt' - ], description=dedent(f"""\ - Running the following commands: - - `python3 -m venv .env`: Create a Python virtual environment - - `source .env/bin/activate`: Activate the virtual environment - - `pip install dlt`: Install dlt - - `dlt init {source_name} duckdb`: Create a new dlt pipeline called {source_name} that loads data into a local DuckDB instance - - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline"""), name="Setup Python environment") - - # editing the resource function to call the requested API - resource_function_range = Range.from_shorthand(15, 0, 30, 0) - await sdk.ide.highlightCode(RangeInFile(filepath=os.path.join(await sdk.ide.getWorkspaceDirectory(), filename), range=resource_function_range), "#ffa50033") - - # sdk.set_loading_message("Writing code to call the API...") - await sdk.edit_file( - range=resource_function_range, - filename=filename, - prompt=f'Edit the resource function to call the API described by this: {self.api_description}. Do not move or remove the exit() call in __main__.', - name=f"Edit the resource function to call the API {AI_ASSISTED_STRING}" - ) - - time.sleep(1) - - # wait for user to put API key in secrets.toml - await sdk.ide.setFileOpen(await sdk.ide.getWorkspaceDirectory() + "/.dlt/secrets.toml") - await sdk.wait_for_user_confirmation("If this service requires an API key, please add it to the `secrets.toml` file and then press `Continue`.") - - sdk.context.set("source_name", source_name) - - -class ValidatePipelineStep(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - workspace_dir = await sdk.ide.getWorkspaceDirectory() - source_name = sdk.context.get("source_name") - filename = f'{source_name}.py' - - # await sdk.run_step(MessageStep(name="Validate the pipeline", message=dedent("""\ - # Next, we will validate that your dlt pipeline is working as expected: - # - Test that the API call works - # - Load the data into a local DuckDB instance - # - Write a query to view the data - # """))) - - # test that the API call works - output = await sdk.run(f'python3 {filename}', name="Test the pipeline", description=f"Running `python3 {filename}` to test loading data from the API", handle_error=False) - - # If it fails, return the error - if "Traceback" in output or "SyntaxError" in output: - output = "Traceback" + output.split("Traceback")[-1] - file_content = await sdk.ide.readFile(os.path.join(workspace_dir, filename)) - suggestion = await sdk.models.gpt35.complete(dedent(f"""\ - ```python - {file_content} - ``` - This above code is a dlt pipeline that loads data from an API. The function with the @resource decorator is responsible for calling the API and returning the data. While attempting to run the pipeline, the following error occurred: - - ```ascii - {output} - ``` - - This is a brief summary of the error followed by a suggestion on how it can be fixed by editing the resource function:""")) - - api_documentation_url = await sdk.models.gpt35.complete(dedent(f"""\ - The API I am trying to call is the '{sdk.context.get('api_description')}'. I tried calling it in the @resource function like this: - ```python - {file_content} - ``` - What is the URL for the API documentation that will help me learn how to make this call? Please format in markdown so I can click the link.""")) - - sdk.raise_exception( - title=f"Error while running pipeline.\nFix the resource function in {filename} and rerun this step", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=dedent(f"""\ - {suggestion} - - {api_documentation_url} - - After you've fixed the code, click the retry button at the top of the Validate Pipeline step above."""))) - - # remove exit() from the main main function - await sdk.run_step(MessageStep(name="Remove early exit() from main function", message="Remove the early exit() from the main function now that we are done testing and want the pipeline to load the data into DuckDB.")) - - contents = await sdk.ide.readFile(os.path.join(workspace_dir, filename)) - replacement = "\n".join( - list(filter(lambda line: line.strip() != "exit()", contents.split("\n")))) - await sdk.ide.applyFileSystemEdit(FileEdit( - filepath=os.path.join(workspace_dir, filename), - replacement=replacement, - range=Range.from_entire_file(contents) - )) - - # load the data into the DuckDB instance - await sdk.run(f'python3 {filename}', name="Load data into DuckDB", description=f"Running python3 {filename} to load data into DuckDB") - - tables_query_code = dedent(f'''\ - import duckdb - - # connect to DuckDB instance - conn = duckdb.connect(database="{source_name}.duckdb") - - # list all tables - print(conn.sql("DESCRIBE"))''') - - query_filename = os.path.join(workspace_dir, "query.py") - await sdk.apply_filesystem_edit(AddFile(filepath=query_filename, content=tables_query_code), name="Add query.py file", description="Adding a file called `query.py` to the workspace that will run a test query on the DuckDB instance") - - -class RunQueryStep(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - output = await sdk.run('.env/bin/python3 query.py', name="Run test query", description="Running `.env/bin/python3 query.py` to test that the data was loaded into DuckDB as expected", handle_error=False) - - if "Traceback" in output or "SyntaxError" in output: - suggestion = await sdk.models.gpt35.complete(dedent(f"""\ - ```python - {await sdk.ide.readFile(os.path.join(sdk.ide.workspace_directory, "query.py"))} - ``` - This above code is a query that runs on the DuckDB instance. While attempting to run the query, the following error occurred: - - ```ascii - {output} - ``` - - This is a brief summary of the error followed by a suggestion on how it can be fixed:""")) - - sdk.raise_exception( - title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion + "\n\nIt is also very likely that no duckdb table was created, which can happen if the resource function did not yield any data. Please make sure that it is yielding data and then rerun this step.") - ) diff --git a/continuedev/src/continuedev/recipes/DDtoBQRecipe/README.md b/continuedev/src/continuedev/recipes/DDtoBQRecipe/README.md deleted file mode 100644 index c4981e56..00000000 --- a/continuedev/src/continuedev/recipes/DDtoBQRecipe/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# DDtoBQRecipe - -Move from using DuckDB to Google BigQuery as the destination for your `dlt` pipeline \ No newline at end of file diff --git a/continuedev/src/continuedev/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md b/continuedev/src/continuedev/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md deleted file mode 100644 index eb68e117..00000000 --- a/continuedev/src/continuedev/recipes/DDtoBQRecipe/dlt_duckdb_to_bigquery_docs.md +++ /dev/null @@ -1,85 +0,0 @@ -### Credentials Missing: ConfigFieldMissingException - -You'll see this exception if `dlt` cannot find your bigquery credentials. In the exception below all of them ('project_id', 'private_key', 'client_email') are missing. The exception gives you also the list of all lookups for configuration performed - [here we explain how to read such list](run-a-pipeline.md#missing-secret-or-configuration-values). - -``` -dlt.common.configuration.exceptions.ConfigFieldMissingException: Following fields are missing: ['project_id', 'private_key', 'client_email'] in configuration with spec GcpServiceAccountCredentials - for field "project_id" config providers and keys were tried in following order: - In Environment Variables key WEATHERAPI__DESTINATION__BIGQUERY__CREDENTIALS__PROJECT_ID was not found. - In Environment Variables key WEATHERAPI__DESTINATION__CREDENTIALS__PROJECT_ID was not found. -``` - -The most common cases for the exception: - -1. The secrets are not in `secrets.toml` at all -2. The are placed in wrong section. For example the fragment below will not work: - -```toml -[destination.bigquery] -project_id = "project_id" # please set me up! -``` - -3. You run the pipeline script from the **different** folder from which it is saved. For example `python weatherapi_demo/weatherapi.py` will run the script from `weatherapi_demo` folder but the current working directory is folder above. This prevents `dlt` from finding `weatherapi_demo/.dlt/secrets.toml` and filling-in credentials. - -### Placeholders still in secrets.toml - -Here BigQuery complain that the format of the `private_key` is incorrect. Practically this most often happens if you forgot to replace the placeholders in `secrets.toml` with real values - -``` - -Connection with BigQuerySqlClient to dataset name weatherapi_data failed. Please check if you configured the credentials at all and provided the right credentials values. You can be also denied access or your internet connection may be down. The actual reason given is: No key could be detected. -``` - -### Bigquery not enabled - -[You must enable Bigquery API.](https://console.cloud.google.com/apis/dashboard) - -``` - -403 POST https://bigquery.googleapis.com/bigquery/v2/projects/bq-walkthrough/jobs?prettyPrint=false: BigQuery API has not been used in project 364286133232 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/bigquery.googleapis.com/overview?project=364286133232 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. - -Location: EU -Job ID: a5f84253-3c10-428b-b2c8-1a09b22af9b2 - [{'@type': 'type.googleapis.com/google.rpc.Help', 'links': [{'description': 'Google developers console API activation', 'url': 'https://console.developers.google.com/apis/api/bigquery.googleapis.com/overview?project=364286133232'}]}, {'@type': 'type.googleapis.com/google.rpc.ErrorInfo', 'reason': 'SERVICE_DISABLED', 'domain': 'googleapis.com', 'metadata': {'service': 'bigquery.googleapis.com', 'consumer': 'projects/364286133232'}}] -``` - -### Lack of permissions to create jobs - -Add `BigQuery Job User` as described in the [destination page](../destinations/bigquery.md). - -``` - -403 POST https://bigquery.googleapis.com/bigquery/v2/projects/bq-walkthrough/jobs?prettyPrint=false: Access Denied: Project bq-walkthrough: User does not have bigquery.jobs.create permission in project bq-walkthrough. - -Location: EU -Job ID: c1476d2c-883c-43f7-a5fe-73db195e7bcd -``` - -### Lack of permissions to query/write data - -Add `BigQuery Data Editor` as described in the [destination page](../destinations/bigquery.md). - -``` - -403 Access Denied: Table bq-walkthrough:weatherapi_data._dlt_loads: User does not have permission to query table bq-walkthrough:weatherapi_data._dlt_loads, or perhaps it does not exist in location EU. - -Location: EU -Job ID: 299a92a3-7761-45dd-a433-79fdeb0c1a46 -``` - -### Lack of billing / BigQuery in sandbox mode - -`dlt` does not support BigQuery when project has no billing enabled. If you see a stack trace where following warning appears: - -``` - -403 Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. DML queries are not allowed in the free tier. Set up a billing account to remove this restriction. -``` - -or - -``` -2023-06-08 16:16:26,769|[WARNING ]|8096|dlt|load.py|complete_jobs:198|Job for weatherapi_resource_83b8ac9e98_4_jsonl retried in load 1686233775.932288 with message {"error_result":{"reason":"billingNotEnabled","message":"Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. Table expiration time must be less than 60 days while in sandbox mode."},"errors":[{"reason":"billingNotEnabled","message":"Billing has not been enabled for this project. Enable billing at https://console.cloud.google.com/billing. Table expiration time must be less than 60 days while in sandbox mode."}],"job_start":"2023-06-08T14:16:26.850000Z","job_end":"2023-06-08T14:16:26.850000Z","job_id":"weatherapi_resource_83b8ac9e98_4_jsonl"} -``` - -you must enable the billing. diff --git a/continuedev/src/continuedev/recipes/DDtoBQRecipe/main.py b/continuedev/src/continuedev/recipes/DDtoBQRecipe/main.py deleted file mode 100644 index 1ae84310..00000000 --- a/continuedev/src/continuedev/recipes/DDtoBQRecipe/main.py +++ /dev/null @@ -1,27 +0,0 @@ -from textwrap import dedent - -from ...core.main import Step -from ...core.sdk import ContinueSDK -from ...steps.core.core import WaitForUserInputStep -from ...steps.core.core import MessageStep -from .steps import SetUpChessPipelineStep, SwitchDestinationStep, LoadDataStep - -# Based on the following guide: -# https://github.com/dlt-hub/dlt/pull/392 - - -class DDtoBQRecipe(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - await sdk.run_step( - MessageStep(name="Move from using DuckDB to Google BigQuery as the destination", message=dedent("""\ - This recipe will walk you through the process of moving from using DuckDB to Google BigQuery as the destination for your dlt pipeline. With the help of Continue, you will: - - Set up a dlt pipeline for the chess.com API - - Switch destination from DuckDB to Google BigQuery - - Add BigQuery credentials to your secrets.toml file - - Run the pipeline again to load data to BigQuery""")) >> - SetUpChessPipelineStep() >> - SwitchDestinationStep() >> - LoadDataStep() - ) diff --git a/continuedev/src/continuedev/recipes/DDtoBQRecipe/steps.py b/continuedev/src/continuedev/recipes/DDtoBQRecipe/steps.py deleted file mode 100644 index df414e2e..00000000 --- a/continuedev/src/continuedev/recipes/DDtoBQRecipe/steps.py +++ /dev/null @@ -1,100 +0,0 @@ -import os -import subprocess -from textwrap import dedent -import time - -from ...steps.find_and_replace import FindAndReplaceStep -from ...models.main import Range -from ...models.filesystem import RangeInFile -from ...steps.core.core import MessageStep -from ...core.sdk import Models -from ...core.observation import DictObservation, InternalErrorObservation -from ...models.filesystem_edit import AddFile, FileEdit -from ...core.main import Step -from ...core.sdk import ContinueSDK - -AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" - - -class SetUpChessPipelineStep(Step): - hide: bool = True - name: str = "Setup Chess.com API dlt Pipeline" - - async def describe(self, models: Models): - return "This step will create a new dlt pipeline that loads data from the chess.com API." - - async def run(self, sdk: ContinueSDK): - - # running commands to get started when creating a new dlt pipeline - await sdk.run([ - 'python3 -m venv .env', - 'source .env/bin/activate', - 'pip install dlt', - 'dlt --non-interactive init chess duckdb', - 'pip install -r requirements.txt', - ], name="Set up Python environment", description=dedent(f"""\ - Running the following commands: - - `python3 -m venv .env`: Create a Python virtual environment - - `source .env/bin/activate`: Activate the virtual environment - - `pip install dlt`: Install dlt - - `dlt init chess duckdb`: Create a new dlt pipeline called "chess" that loads data into a local DuckDB instance - - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline""")) - - -class SwitchDestinationStep(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - - # Switch destination from DuckDB to Google BigQuery - filepath = os.path.join( - sdk.ide.workspace_directory, 'chess_pipeline.py') - await sdk.run_step(FindAndReplaceStep(filepath=filepath, pattern="destination='duckdb'", replacement="destination='bigquery'")) - - # Add BigQuery credentials to your secrets.toml file - template = dedent(f"""\ - [destination.bigquery.credentials] - location = "US" # change the location of the data - project_id = "project_id" # please set me up! - private_key = "private_key" # please set me up! - client_email = "client_email" # please set me up!""") - - # wait for user to put API key in secrets.toml - secrets_path = os.path.join( - sdk.ide.workspace_directory, ".dlt/secrets.toml") - await sdk.ide.setFileOpen(secrets_path) - await sdk.append_to_file(secrets_path, template) - - # append template to bottom of secrets.toml - await sdk.wait_for_user_confirmation("Please add your GCP credentials to `secrets.toml` file and then press `Continue`") - - -class LoadDataStep(Step): - name: str = "Load data to BigQuery" - hide: bool = True - - async def run(self, sdk: ContinueSDK): - # Run the pipeline again to load data to BigQuery - output = await sdk.run('.env/bin/python3 chess_pipeline.py', name="Load data to BigQuery", description="Running `.env/bin/python3 chess_pipeline.py` to load data to Google BigQuery") - - if "Traceback" in output or "SyntaxError" in output: - with open(os.path.join(os.path.dirname(__file__), "dlt_duckdb_to_bigquery_docs.md"), "r") as f: - docs = f.read() - - output = "Traceback" + output.split("Traceback")[-1] - suggestion = await sdk.models.default.complete(dedent(f"""\ - When trying to load data into BigQuery, the following error occurred: - - ```ascii - {output} - ``` - - Here is documentation describing common errors and their causes/solutions: - - {docs} - - This is a brief summary of the error followed by a suggestion on how it can be fixed:""")) - - sdk.raise_exception( - title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion) - ) diff --git a/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/README.md b/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/main.py b/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/main.py deleted file mode 100644 index 2a3e3566..00000000 --- a/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/main.py +++ /dev/null @@ -1,62 +0,0 @@ -from textwrap import dedent - -from ...steps.input.nl_multiselect import NLMultiselectStep -from ...core.main import Step -from ...core.sdk import ContinueSDK -from ...steps.core.core import WaitForUserInputStep -from ...steps.core.core import MessageStep -from .steps import SetupPipelineStep, DeployAirflowStep, RunPipelineStep - - -# https://github.com/dlt-hub/dlt-deploy-template/blob/master/airflow-composer/dag_template.py -# https://www.notion.so/dlthub/Deploy-a-pipeline-with-Airflow-245fd1058652479494307ead0b5565f3 -# 1. What verified pipeline do you want to deploy with Airflow? -# 2. Set up selected verified pipeline -# 3. Deploy selected verified pipeline with Airflow -# 4. Set up Airflow locally? - - -class DeployPipelineAirflowRecipe(Step): - hide: bool = True - - async def run(self, sdk: ContinueSDK): - source_name = await sdk.run_step( - MessageStep(name="Deploying a pipeline to Airflow", message=dedent("""\ - This recipe will show you how to deploy a pipeline to Airflow. With the help of Continue, you will: - - Select a dlt-verified pipeline - - Setup the pipeline - - Deploy it to Airflow - - Optionally, setup Airflow locally""")) >> - NLMultiselectStep( - prompt=dedent("""\ - Which verified pipeline do you want to deploy with Airflow? The options are: - - Asana - - Chess.com - - Facebook Ads - - GitHub - - Google Analytics - - Google Sheets - - HubSpot - - Jira - - Matomo - - Mux - - Notion - - Pipedrive - - Pokemon - - Salesforce - - Shopify - - Strapi - - Stripe - - SQL Database - - Workable - - Zendesk"""), - options=[ - "asana_dlt", "chess", "github", "google_analytics", "google_sheets", "hubspot", "matomo", "pipedrive", "shopify_dlt", "strapi", "zendesk", - "facebook_ads", "jira", "mux", "notion", "pokemon", "salesforce", "stripe_analytics", "sql_database", "workable" - ]) - ) - await sdk.run_step( - SetupPipelineStep(source_name=source_name) >> - RunPipelineStep(source_name=source_name) >> - DeployAirflowStep(source_name=source_name) - ) diff --git a/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/steps.py b/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/steps.py deleted file mode 100644 index d9bdbc0a..00000000 --- a/continuedev/src/continuedev/recipes/DeployPipelineAirflowRecipe/steps.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import subprocess -from textwrap import dedent -import time - -from ...steps.core.core import WaitForUserInputStep -from ...models.main import Range -from ...models.filesystem import RangeInFile -from ...steps.core.core import MessageStep -from ...core.sdk import Models -from ...core.observation import DictObservation, InternalErrorObservation -from ...models.filesystem_edit import AddFile, FileEdit -from ...core.main import Step -from ...core.sdk import ContinueSDK -from ...steps.find_and_replace import FindAndReplaceStep - -AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" - - -class SetupPipelineStep(Step): - hide: bool = True - name: str = "Setup dlt Pipeline" - - source_name: str - - async def describe(self, models: Models): - pass - - async def run(self, sdk: ContinueSDK): - await sdk.run([ - 'python3 -m venv .env', - 'source .env/bin/activate', - 'pip install dlt', - f'dlt --non-interactive init {self.source_name} duckdb', - 'pip install -r requirements.txt' - ], description=dedent(f"""\ - Running the following commands: - - `python3 -m venv .env`: Create a Python virtual environment - - `source .env/bin/activate`: Activate the virtual environment - - `pip install dlt`: Install dlt - - `dlt init {self.source_name} duckdb`: Create a new dlt pipeline called {self.source_name} that loads data into a local DuckDB instance - - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline"""), name="Setup Python environment") - - -class RunPipelineStep(Step): - hide: bool = True - name: str = "Run dlt Pipeline" - - source_name: str - - async def describe(self, models: Models): - pass - - async def run(self, sdk: ContinueSDK): - await sdk.run([ - f'python3 {self.source_name}_pipeline.py', - ], description=dedent(f"""\ - Running the command `python3 {self.source_name}_pipeline.py to run the pipeline: """), name="Run dlt pipeline") - - -class DeployAirflowStep(Step): - hide: bool = True - source_name: str - - async def run(self, sdk: ContinueSDK): - - # Run dlt command to deploy pipeline to Airflow - await sdk.run( - ['git init', - f'dlt --non-interactive deploy {self.source_name}_pipeline.py airflow-composer'], - description="Running `dlt deploy airflow` to deploy the dlt pipeline to Airflow", name="Deploy dlt pipeline to Airflow") - - # Get filepaths, open the DAG file - directory = await sdk.ide.getWorkspaceDirectory() - pipeline_filepath = os.path.join( - directory, f"{self.source_name}_pipeline.py") - dag_filepath = os.path.join( - directory, f"dags/dag_{self.source_name}_pipeline.py") - - await sdk.ide.setFileOpen(dag_filepath) - - # Replace the pipeline name and dataset name - await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="'pipeline_name'", replacement=f"'{self.source_name}_pipeline'")) - await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="'dataset_name'", replacement=f"'{self.source_name}_data'")) - await sdk.run_step(FindAndReplaceStep(filepath=pipeline_filepath, pattern="pipeline_or_source_script", replacement=f"{self.source_name}_pipeline")) - - # Prompt the user for the DAG schedule - # edit_dag_range = Range.from_shorthand(18, 0, 23, 0) - # await sdk.ide.highlightCode(range_in_file=RangeInFile(filepath=dag_filepath, range=edit_dag_range), color="#33993333") - # response = await sdk.run_step(WaitForUserInputStep(prompt="When would you like this Airflow DAG to run? (e.g. every day, every Monday, every 1st of the month, etc.)")) - # await sdk.edit_file(dag_filepath, prompt=f"Edit the DAG so that it runs at the following schedule: '{response.text}'", - # range=edit_dag_range) - - # Tell the user to check the schedule and fill in owner, email, other default_args - await sdk.run_step(MessageStep(message="Fill in the owner, email, and other default_args in the DAG file with your own personal information. Then the DAG will be ready to run!", name="Fill in default_args")) - - # Run the DAG locally ?? diff --git a/continuedev/src/continuedev/recipes/README.md b/continuedev/src/continuedev/recipes/README.md deleted file mode 100644 index d5a006fb..00000000 --- a/continuedev/src/continuedev/recipes/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# This is a collaborative collection of Continue recipes - -Recipes here will automatically be made available in the [Continue VS Code extension](https://marketplace.visualstudio.com/items?itemName=Continue.continue). - -The `recipes` folder contains all recipes, each with the same structure. **If you wish to create your own recipe, please do the following:** - -1. Create a new subfolder in `recipes`, with the name of your recipe (for example `MyNewRecipe`). -2. Make 2 files in this folder: 1) a `README.md` describing your recipe and how to use it and 2) a `main.py` including a single class with the name of your recipe (e.g. `MyNewRecipe`). -3. Write any utility code other than the main recipe class in a separate file, which you can import in `main.py`. Particularly if you decide to break the recipe into multiple sub-steps, try to keep these separate. - -# Existing Recipes - -`ContinueRecipeRecipe` - Write a Continue recipe with Continue. - -`CreatePipelineRecipe` - Build a dlt pipeline from scratch for an API of your choice. - -`WritePytestsRecipe` - Write Pytest unit tests in a folder adjacent to your Python file. diff --git a/continuedev/src/continuedev/recipes/TemplateRecipe/README.md b/continuedev/src/continuedev/recipes/TemplateRecipe/README.md deleted file mode 100644 index 91d1123b..00000000 --- a/continuedev/src/continuedev/recipes/TemplateRecipe/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# TemplateRecipe - -This folder is a template that you can copy to create your own recipe. - -## How to use this recipe - -Explain here what users should know when using your recipe. What inputs does it have and what actions will it perform? diff --git a/continuedev/src/continuedev/recipes/TemplateRecipe/main.py b/continuedev/src/continuedev/recipes/TemplateRecipe/main.py deleted file mode 100644 index 16132cfd..00000000 --- a/continuedev/src/continuedev/recipes/TemplateRecipe/main.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Coroutine -from continuedev.core import Step, ContinueSDK, Observation, Models - - -class TemplateRecipe(Step): - """ - A simple recipe that appends a print statement to the currently open file. - Use this as a template to create your own! - """ - - # Paremeters for the recipe - name: str - - # A title for the recipe, to be displayed in the GUI - title = "Template Recipe" - - # A description of what the recipe accomplished, to be displayed in the GUI - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return f"Appended a statement to print `Hello, {self.name}!` at the end of the file." - - # The code executed when the recipe is run - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - visible_files = await sdk.ide.getVisibleFiles() - await sdk.edit_file( - filename=visible_files[0], - prompt=f"Append a statement to print `Hello, {self.name}!` at the end of the file." - ) diff --git a/continuedev/src/continuedev/recipes/WritePytestsRecipe/README.md b/continuedev/src/continuedev/recipes/WritePytestsRecipe/README.md deleted file mode 100644 index 5ce33ecb..00000000 --- a/continuedev/src/continuedev/recipes/WritePytestsRecipe/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# CreatePytestsRecipe - -A recipe for writing unit tests in Pytest. - -# How to use this recipe - -Call this recipe with a python file open that you would like to test. It will create tests in a `tests/` folder adjacent to the file with the test file given the same name prepended by `test_`. diff --git a/continuedev/src/continuedev/recipes/WritePytestsRecipe/main.py b/continuedev/src/continuedev/recipes/WritePytestsRecipe/main.py deleted file mode 100644 index c7a65fa6..00000000 --- a/continuedev/src/continuedev/recipes/WritePytestsRecipe/main.py +++ /dev/null @@ -1,48 +0,0 @@ -from textwrap import dedent -from typing import Union -from ...models.filesystem_edit import AddDirectory, AddFile -from ...core.main import Step, ContinueSDK -import os - - -class WritePytestsRecipe(Step): - for_filepath: Union[str, None] = None - user_input: str = "Write unit tests for this file." - - async def describe(self, models): - return f"Writing unit tests for {self.for_filepath}" - - async def run(self, sdk: ContinueSDK): - if self.for_filepath is None: - self.for_filepath = (await sdk.ide.getVisibleFiles())[0] - - filename = os.path.basename(self.for_filepath) - dirname = os.path.dirname(self.for_filepath) - - path_dir = os.path.join(dirname, "tests") - if not os.path.exists(path_dir): - await sdk.apply_filesystem_edit(AddDirectory(path=path_dir)) - - path = os.path.join(path_dir, f"test_{filename}") - if os.path.exists(path): - return None - - for_file_contents = await sdk.ide.readFile(self.for_filepath) - - prompt = dedent(f"""\ - This is the file you will write unit tests for: - - ```python - {for_file_contents} - ``` - - Here are additional instructions: - - "{self.user_input}" - - Here is a complete set of pytest unit tests:""") - tests = await sdk.models.gpt35.complete(prompt) - - await sdk.apply_filesystem_edit(AddFile(filepath=path, content=tests)) - - return None diff --git a/continuedev/src/continuedev/steps/README.md b/continuedev/src/continuedev/steps/README.md deleted file mode 100644 index 12073835..00000000 --- a/continuedev/src/continuedev/steps/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Steps - -Steps are the composable unit of action in Continue. They define a `run` method which has access to the entire `ContinueSDK`, allowing you to take actions inside the IDE, call language models, and more. In this folder you can find a number of good examples. - -## How to write a step - -a. Start by creating a subclass of `Step` - -You should first consider what will be the parameters of your recipe. These are defined as attributes in the Pydantic class. For example, if you wanted a "filepath" attribute that would look like this: - -```python -class HelloWorldStep(Step): - filepath: str - ... -``` - -b. Next, write the `run` method - -This method takes the ContinueSDK as a parameter, giving you all the tools you need to write your steps (if it's missing something, let us know, we'll add it!). You can write any code inside the run method; this is what will happen when your step is run, line for line. As an example, here's a step that will open a file and append "Hello World!": - -```python -class HelloWorldStep(Step): - filepath: str - - async def run(self, sdk: ContinueSDK): - await sdk.ide.setFileOpen(self.filepath) - await sdk.append_to_file(self.filepath, "Hello World!") -``` - -c. Finally, every Step is displayed with a description of what it has done - -If you'd like to override the default description of your step, which is just the class name, then implement the `describe` method. You can: - -- Return a static string -- Store state in a class attribute (prepend with a double underscore, which signifies (through Pydantic) that this is not a parameter for the Step, just internal state) during the run method, and then grab this in the describe method. -- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.gpt35.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`. - -Here's an example: - -```python -class HelloWorldStep(Step): - filepath: str - - async def run(self, sdk: ContinueSDK): - await sdk.ide.setFileOpen(self.filepath) - await sdk.append_to_file(self.filepath, "Hello World!") - - def describe(self, models: Models): - return f"Appended 'Hello World!' to {self.filepath}" -``` diff --git a/continuedev/src/continuedev/steps/__init__.py b/continuedev/src/continuedev/steps/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/continuedev/src/continuedev/steps/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py deleted file mode 100644 index aade1ea1..00000000 --- a/continuedev/src/continuedev/steps/chat.py +++ /dev/null @@ -1,270 +0,0 @@ -import json -from typing import Any, Coroutine, List - -from pydantic import Field - -from ..libs.util.strings import remove_quotes_and_escapes -from .main import EditHighlightedCodeStep -from .core.core import MessageStep -from ..core.main import FunctionCall, Models -from ..core.main import ChatMessage, Step, step_to_json_schema -from ..core.sdk import ContinueSDK -import openai -import os -from dotenv import load_dotenv -from directory_tree import display_tree - -load_dotenv() -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") -openai.api_key = OPENAI_API_KEY - - -class SimpleChatStep(Step): - name: str = "Generating Response..." - manage_own_chat_context: bool = True - description: str = "" - messages: List[ChatMessage] = None - - async def run(self, sdk: ContinueSDK): - completion = "" - messages = self.messages or await sdk.get_chat_context() - - generator = sdk.models.default.stream_chat( - messages, temperature=sdk.config.temperature) - try: - async for chunk in generator: - if sdk.current_step_was_deleted(): - # So that the message doesn't disappear - self.hide = False - break - - if "content" in chunk: - self.description += chunk["content"] - completion += chunk["content"] - await sdk.update_ui() - finally: - self.name = remove_quotes_and_escapes(await sdk.models.gpt35.complete( - f"Write a short title for the following chat message: {self.description}")) - - self.chat_context.append(ChatMessage( - role="assistant", - content=completion, - summary=self.name - )) - - # TODO: Never actually closing. - await generator.aclose() - - -class AddFileStep(Step): - name: str = "Add File" - description = "Add a file to the workspace. Should always view the directory tree before this." - filename: str - file_contents: str - - async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: - return f"Added a file named `{self.filename}` to the workspace." - - async def run(self, sdk: ContinueSDK): - await sdk.add_file(self.filename, self.file_contents) - - await sdk.ide.setFileOpen(os.path.join(sdk.ide.workspace_directory, self.filename)) - - -class DeleteFileStep(Step): - name: str = "Delete File" - description = "Delete a file from the workspace." - filename: str - - async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: - return f"Deleted a file named `{self.filename}` from the workspace." - - async def run(self, sdk: ContinueSDK): - await sdk.delete_file(self.filename) - - -class AddDirectoryStep(Step): - name: str = "Add Directory" - description = "Add a directory to the workspace." - directory_name: str - - async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: - return f"Added a directory named `{self.directory_name}` to the workspace." - - async def run(self, sdk: ContinueSDK): - try: - await sdk.add_directory(self.directory_name) - except FileExistsError: - self.description = f"Directory {self.directory_name} already exists." - - -class RunTerminalCommandStep(Step): - name: str = "Run Terminal Command" - description: str = "Run a terminal command." - command: str - - async def run(self, sdk: ContinueSDK): - self.description = f"Copy this command and run in your terminal:\n\n```bash\n{self.command}\n```" - - -class ViewDirectoryTreeStep(Step): - name: str = "View Directory Tree" - description: str = "View the directory tree to learn which folder and files exist. You should always do this before adding new files." - - async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: - return f"Viewed the directory tree." - - async def run(self, sdk: ContinueSDK): - self.description = f"```\n{display_tree(sdk.ide.workspace_directory, True, max_depth=2)}\n```" - - -class EditFileStep(Step): - name: str = "Edit File" - description: str = "Edit a file in the workspace that is not currently open." - filename: str = Field( - ..., description="The name of the file to edit.") - instructions: str = Field( - ..., description="The instructions to edit the file.") - hide: bool = True - - async def run(self, sdk: ContinueSDK): - await sdk.edit_file(self.filename, self.instructions) - - -class ChatWithFunctions(Step): - user_input: str - functions: List[Step] = [AddFileStep(filename="", file_contents=""), - EditFileStep(filename="", instructions=""), - EditHighlightedCodeStep(user_input=""), - ViewDirectoryTreeStep(), AddDirectoryStep(directory_name=""), - DeleteFileStep(filename=""), RunTerminalCommandStep(command="")] - name: str = "Input" - manage_own_chat_context: bool = True - description: str = "" - hide: bool = True - - async def run(self, sdk: ContinueSDK): - await sdk.update_ui() - - step_name_step_class_map = { - step.name.replace(" ", ""): step.__class__ for step in self.functions} - - functions = [step_to_json_schema( - function) for function in self.functions] - - self.chat_context.append(ChatMessage( - role="user", - content=self.user_input, - summary=self.user_input - )) - - last_function_called_index_in_history = None - last_function_called_name = None - last_function_called_params = None - while True: - was_function_called = False - func_args = "" - func_name = "" - msg_content = "" - msg_step = None - - async for msg_chunk in sdk.models.gpt350613.stream_chat(await sdk.get_chat_context(), functions=functions): - if sdk.current_step_was_deleted(): - return - - if "content" in msg_chunk and msg_chunk["content"] is not None: - msg_content += msg_chunk["content"] - # if last_function_called_index_in_history is not None: - # while sdk.history.timeline[last_function_called_index].step.hide: - # last_function_called_index += 1 - # sdk.history.timeline[last_function_called_index_in_history].step.description = msg_content - if msg_step is None: - msg_step = MessageStep( - name="Chat", - message=msg_chunk["content"] - ) - await sdk.run_step(msg_step) - else: - msg_step.description = msg_content - await sdk.update_ui() - elif "function_call" in msg_chunk or func_name != "": - was_function_called = True - if "function_call" in msg_chunk: - if "arguments" in msg_chunk["function_call"]: - func_args += msg_chunk["function_call"]["arguments"] - if "name" in msg_chunk["function_call"]: - func_name += msg_chunk["function_call"]["name"] - - if not was_function_called: - self.chat_context.append(ChatMessage( - role="assistant", - content=msg_content, - summary=msg_content - )) - break - else: - last_function_called = func_name - if func_name == "python" and "python" not in step_name_step_class_map: - # GPT must be fine-tuned to believe this exists, but it doesn't always - func_name = "EditHighlightedCodeStep" - func_args = json.dumps({"user_input": self.user_input}) - # self.chat_context.append(ChatMessage( - # role="assistant", - # content=None, - # function_call=FunctionCall( - # name=func_name, - # arguments=func_args - # ), - # summary=f"Called function {func_name}" - # )) - # self.chat_context.append(ChatMessage( - # role="user", - # content="The 'python' function does not exist. Don't call it. Try again to call another function.", - # summary="'python' function does not exist." - # )) - # msg_step.hide = True - # continue - # Call the function, then continue to chat - func_args = "{}" if func_args == "" else func_args - try: - fn_call_params = json.loads(func_args) - except json.JSONDecodeError: - raise Exception( - "The model returned invalid JSON. Please try again") - self.chat_context.append(ChatMessage( - role="assistant", - content=None, - function_call=FunctionCall( - name=func_name, - arguments=func_args - ), - summary=f"Called function {func_name}" - )) - last_function_called_index_in_history = sdk.history.current_index + 1 - if func_name not in step_name_step_class_map: - raise Exception( - f"The model tried to call a function ({func_name}) that does not exist. Please try again.") - - # if func_name == "AddFileStep": - # step_to_run.hide = True - # self.description += f"\nAdded file `{func_args['filename']}`" - # elif func_name == "AddDirectoryStep": - # step_to_run.hide = True - # self.description += f"\nAdded directory `{func_args['directory_name']}`" - # else: - # self.description += f"\n`Running function {func_name}`\n\n" - if func_name == "EditHighlightedCodeStep": - fn_call_params["user_input"] = self.user_input - elif func_name == "EditFile": - fn_call_params["instructions"] = self.user_input - - step_to_run = step_name_step_class_map[func_name]( - **fn_call_params) - if last_function_called_name is not None and last_function_called_name == func_name and last_function_called_params is not None and last_function_called_params == fn_call_params: - # If it's calling the same function more than once in a row, it's probably looping and confused - return - last_function_called_name = func_name - last_function_called_params = fn_call_params - - await sdk.run_step(step_to_run) - await sdk.update_ui() diff --git a/continuedev/src/continuedev/steps/chroma.py b/continuedev/src/continuedev/steps/chroma.py deleted file mode 100644 index 9d085981..00000000 --- a/continuedev/src/continuedev/steps/chroma.py +++ /dev/null @@ -1,80 +0,0 @@ -from textwrap import dedent -from typing import Coroutine, Union -from ..core.observation import Observation, TextObservation -from ..core.main import Step -from ..core.sdk import ContinueSDK -from .core.core import EditFileStep -from ..libs.chroma.query import ChromaIndexManager -from .core.core import EditFileStep - - -class CreateCodebaseIndexChroma(Step): - name: str = "Create Codebase Index" - hide: bool = True - - async def describe(self, llm) -> Coroutine[str, None, None]: - return "Indexing the codebase..." - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) - if not index.check_index_exists(): - self.hide = False - index.create_codebase_index() - - -class AnswerQuestionChroma(Step): - question: str - _answer: Union[str, None] = None - name: str = "Answer Question" - - async def describe(self, llm) -> Coroutine[str, None, None]: - if self._answer is None: - return f"Answering the question: {self.question}" - else: - return self._answer - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) - results = index.query_codebase_index(self.question) - - code_snippets = "" - - files = [] - for node in results.source_nodes: - resource_name = list(node.node.relationships.values())[0] - filepath = resource_name[:resource_name.index("::")] - files.append(filepath) - code_snippets += f"""{filepath}```\n{node.node.text}\n```\n\n""" - - prompt = dedent(f"""Here are a few snippets of code that might be useful in answering the question: - - {code_snippets} - - Here is the question to answer: - - {self.question} - - Here is the answer:""") - - answer = await sdk.models.gpt35.complete(prompt) - # Make paths relative to the workspace directory - answer = answer.replace(await sdk.ide.getWorkspaceDirectory(), "") - - self._answer = answer - - await sdk.ide.setFileOpen(files[0]) - - -class EditFileChroma(Step): - request: str - hide: bool = True - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - index = ChromaIndexManager(await sdk.ide.getWorkspaceDirectory()) - results = index.query_codebase_index(self.request) - - resource_name = list( - results.source_nodes[0].node.relationships.values())[0] - filepath = resource_name[:resource_name.index("::")] - - await sdk.run_step(EditFileStep(filepath=filepath, prompt=f"Here is the code:\n\n{{code}}\n\nHere is the user request:\n\n{self.request}\n\nHere is the code after making the requested changes:\n")) diff --git a/continuedev/src/continuedev/steps/clear_history.py b/continuedev/src/continuedev/steps/clear_history.py deleted file mode 100644 index a875c6d3..00000000 --- a/continuedev/src/continuedev/steps/clear_history.py +++ /dev/null @@ -1,10 +0,0 @@ -from ..core.main import Step -from ..core.sdk import ContinueSDK - - -class ClearHistoryStep(Step): - name: str = "Clear History" - hide: bool = True - - async def run(self, sdk: ContinueSDK): - await sdk.clear_history() diff --git a/continuedev/src/continuedev/steps/comment_code.py b/continuedev/src/continuedev/steps/comment_code.py deleted file mode 100644 index aa17e62c..00000000 --- a/continuedev/src/continuedev/steps/comment_code.py +++ /dev/null @@ -1,12 +0,0 @@ -from ..core.main import ContinueSDK, Models, Step -from .main import EditHighlightedCodeStep - - -class CommentCodeStep(Step): - hide: bool = True - - async def describe(self, models: Models): - return "Writing comments" - - async def run(self, sdk: ContinueSDK): - await sdk.run_step(EditHighlightedCodeStep(user_input="Write comprehensive comments in the canonical format for every class and function")) diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py deleted file mode 100644 index 98600f8b..00000000 --- a/continuedev/src/continuedev/steps/core/core.py +++ /dev/null @@ -1,731 +0,0 @@ -# These steps are depended upon by ContinueSDK -import os -import subprocess -import difflib -from textwrap import dedent -from typing import Coroutine, List, Literal, Union - -from ...libs.llm.ggml import GGML -from ...models.main import Range -from ...libs.llm.prompt_utils import MarkdownStyleEncoderDecoder -from ...models.filesystem_edit import EditDiff, FileEdit, FileEditWithFullContents, FileSystemEdit -from ...models.filesystem import FileSystem, RangeInFile, RangeInFileWithContents -from ...core.observation import Observation, TextObservation, TracebackObservation, UserInputObservation -from ...core.main import ChatMessage, ContinueCustomException, Step, SequentialStep -from ...libs.util.count_tokens import MAX_TOKENS_FOR_MODEL, DEFAULT_MAX_TOKENS -from ...libs.util.strings import dedent_and_get_common_whitespace, remove_quotes_and_escapes -import difflib - - -class ContinueSDK: - pass - - -class Models: - pass - - -class ReversibleStep(Step): - async def reverse(self, sdk: ContinueSDK): - raise NotImplementedError - - -class MessageStep(Step): - name: str = "Message" - message: str - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return self.message - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - return TextObservation(text=self.message) - - -class FileSystemEditStep(ReversibleStep): - edit: FileSystemEdit - _diff: Union[EditDiff, None] = None - - hide: bool = True - - async def run(self, sdk: "ContinueSDK") -> Coroutine[Observation, None, None]: - self._diff = await sdk.ide.applyFileSystemEdit(self.edit) - return None - - async def reverse(self, sdk: "ContinueSDK"): - await sdk.ide.applyFileSystemEdit(self._diff.backward) - # Where and when should file saves happen? - - -def output_contains_error(output: str) -> bool: - return "Traceback" in output or "SyntaxError" in output - - -AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)" - - -class ShellCommandsStep(Step): - cmds: List[str] - cwd: Union[str, None] = None - name: str = "Run Shell Commands" - handle_error: bool = True - - _err_text: Union[str, None] = None - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - if self._err_text is not None: - return f"Error when running shell commands:\n```\n{self._err_text}\n```" - - cmds_str = "\n".join(self.cmds) - return await models.gpt35.complete(f"{cmds_str}\n\nSummarize what was done in these shell commands, using markdown bullet points:") - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - cwd = await sdk.ide.getWorkspaceDirectory() if self.cwd is None else self.cwd - - for cmd in self.cmds: - output = await sdk.ide.runCommand(cmd) - if self.handle_error and output is not None and output_contains_error(output): - suggestion = await sdk.models.gpt35.complete(dedent(f"""\ - While running the command `{cmd}`, the following error occurred: - - ```ascii - {output} - ``` - - This is a brief summary of the error followed by a suggestion on how it can be fixed:"""), with_history=await sdk.get_chat_context()) - - sdk.raise_exception( - title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=f"{suggestion}\n\nYou can click the retry button on the failed step to try again.") - ) - - return TextObservation(text=output) - - # process = subprocess.Popen( - # '/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd) - - # stdin_input = "\n".join(self.cmds) - # out, err = process.communicate(stdin_input.encode()) - - # # If it fails, return the error - # if err is not None and err != "": - # self._err_text = err - # return TextObservation(text=err) - - # return None - - -class DefaultModelEditCodeStep(Step): - user_input: str - range_in_files: List[RangeInFile] - name: str = "Editing Code" - hide = False - description: str = "" - _prompt: str = dedent("""\ - Take the file prefix and suffix into account, but only rewrite the code_to_edit as specified in the user_request. The code you write in modified_code_to_edit will replace the code between the code_to_edit tags. Do NOT preface your answer or write anything other than code. The tag should be written to indicate the end of the modified code section. Do not ever use nested tags. - - Example: - - - class Database: - def __init__(self): - self._data = {{}} - - def get(self, key): - return self._data[key] - - - - def set(self, key, value): - self._data[key] = value - - - - def clear_all(): - self._data = {{}} - - - Raise an error if the key already exists. - - - def set(self, key, value): - if key in self._data: - raise KeyError(f"Key {{key}} already exists") - self._data[key] = value - - - Main task: - """) - _previous_contents: str = "" - _new_contents: str = "" - _prompt_and_completion: str = "" - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - if self._previous_contents.strip() == self._new_contents.strip(): - description = "No edits were made" - else: - changes = '\n'.join(difflib.ndiff( - self._previous_contents.splitlines(), self._new_contents.splitlines())) - description = await models.gpt3516k.complete(dedent(f"""\ - Diff summary: "{self.user_input}" - - ```diff - {changes} - ``` - - Please give brief a description of the changes made above using markdown bullet points. Be concise:""")) - name = await models.gpt3516k.complete(f"Write a very short title to describe this requested change (no quotes): '{self.user_input}'. This is the title:") - self.name = remove_quotes_and_escapes(name) - - return f"{remove_quotes_and_escapes(description)}" - - async def get_prompt_parts(self, rif: RangeInFileWithContents, sdk: ContinueSDK, full_file_contents: str): - # We don't know here all of the functions being passed in. - # We care because if this prompt itself goes over the limit, then the entire message will have to be cut from the completion. - # Overflow won't happen, but prune_chat_messages in count_tokens.py will cut out this whole thing, instead of us cutting out only as many lines as we need. - model_to_use = sdk.models.default - max_tokens = int(MAX_TOKENS_FOR_MODEL.get( - model_to_use.name, DEFAULT_MAX_TOKENS) / 2) - - TOKENS_TO_BE_CONSIDERED_LARGE_RANGE = 1200 - if model_to_use.count_tokens(rif.contents) > TOKENS_TO_BE_CONSIDERED_LARGE_RANGE: - self.description += "\n\n**It looks like you've selected a large range to edit, which may take a while to complete. If you'd like to cancel, click the 'X' button above. If you highlight a more specific range, Continue will only edit within it.**" - - # At this point, we also increase the max_tokens parameter so it doesn't stop in the middle of generation - # Increase max_tokens to be double the size of the range - # But don't exceed twice default max tokens - max_tokens = int(min(model_to_use.count_tokens( - rif.contents), DEFAULT_MAX_TOKENS) * 2.5) - - BUFFER_FOR_FUNCTIONS = 400 - total_tokens = model_to_use.count_tokens( - full_file_contents + self._prompt + self.user_input) + BUFFER_FOR_FUNCTIONS + max_tokens - - # If using 3.5 and overflows, upgrade to 3.5.16k - if model_to_use.name == "gpt-3.5-turbo": - if total_tokens > MAX_TOKENS_FOR_MODEL["gpt-3.5-turbo"]: - model_to_use = sdk.models.gpt3516k - - # Remove tokens from the end first, and then the start to clear space - # This part finds the start and end lines - full_file_contents_lst = full_file_contents.split("\n") - max_start_line = rif.range.start.line - min_end_line = rif.range.end.line - cur_start_line = 0 - cur_end_line = len(full_file_contents_lst) - 1 - - if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]: - while cur_end_line > min_end_line: - total_tokens -= model_to_use.count_tokens( - full_file_contents_lst[cur_end_line]) - cur_end_line -= 1 - if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]: - break - - if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]: - while cur_start_line < max_start_line: - cur_start_line += 1 - total_tokens -= model_to_use.count_tokens( - full_file_contents_lst[cur_start_line]) - if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]: - break - - # Now use the found start/end lines to get the prefix and suffix strings - file_prefix = "\n".join( - full_file_contents_lst[cur_start_line:max_start_line]) - file_suffix = "\n".join( - full_file_contents_lst[min_end_line:cur_end_line - 1]) - - # Move any surrounding blank line in rif.contents to the prefix/suffix - # TODO: Keep track of start line of the range, because it's needed below for offset stuff - rif_start_line = rif.range.start.line - if len(rif.contents) > 0: - lines = rif.contents.splitlines(keepends=True) - first_line = lines[0] if lines else None - while first_line and first_line.strip() == "": - file_prefix += first_line - rif.contents = rif.contents[len(first_line):] - lines = rif.contents.splitlines(keepends=True) - first_line = lines[0] if lines else None - - last_line = lines[-1] if lines else None - while last_line and last_line.strip() == "": - file_suffix = last_line + file_suffix - rif.contents = rif.contents[:len( - rif.contents) - len(last_line)] - lines = rif.contents.splitlines(keepends=True) - last_line = lines[-1] if lines else None - - while rif.contents.startswith("\n"): - file_prefix += "\n" - rif.contents = rif.contents[1:] - while rif.contents.endswith("\n"): - file_suffix = "\n" + file_suffix - rif.contents = rif.contents[:-1] - - return file_prefix, rif.contents, file_suffix, model_to_use, max_tokens - - def compile_prompt(self, file_prefix: str, contents: str, file_suffix: str, sdk: ContinueSDK) -> str: - if contents.strip() == "": - # Seperate prompt for insertion at the cursor, the other tends to cause it to repeat whole file - prompt = dedent(f"""\ - -{file_prefix} - - - -{file_suffix} - - -{self.user_input} - - -Please output the code to be inserted at the cursor in order to fulfill the user_request. Do NOT preface your answer or write anything other than code. You should not write any tags, just the code. Make sure to correctly indent the code:""") - return prompt - - prompt = self._prompt - if file_prefix.strip() != "": - prompt += dedent(f""" - -{file_prefix} -""") - prompt += dedent(f""" - -{contents} -""") - if file_suffix.strip() != "": - prompt += dedent(f""" - -{file_suffix} -""") - prompt += dedent(f""" - -{self.user_input} - - -""") - - return prompt - - def is_end_line(self, line: str) -> bool: - return "" in line or "" in line - - def line_to_be_ignored(self, line: str, is_first_line: bool = False) -> bool: - return "```" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line or "" in line - - async def stream_rif(self, rif: RangeInFileWithContents, sdk: ContinueSDK): - await sdk.ide.saveFile(rif.filepath) - full_file_contents = await sdk.ide.readFile(rif.filepath) - - file_prefix, contents, file_suffix, model_to_use, max_tokens = await self.get_prompt_parts( - rif, sdk, full_file_contents) - contents, common_whitespace = dedent_and_get_common_whitespace( - contents) - prompt = self.compile_prompt(file_prefix, contents, file_suffix, sdk) - full_file_contents_lines = full_file_contents.split("\n") - - lines_to_display = [] - - async def sendDiffUpdate(lines: List[str], sdk: ContinueSDK, final: bool = False): - nonlocal full_file_contents_lines, rif, lines_to_display - - completion = "\n".join(lines) - - full_prefix_lines = full_file_contents_lines[:rif.range.start.line] - full_suffix_lines = full_file_contents_lines[rif.range.end.line:] - - # Don't do this at the very end, just show the inserted code - if final: - lines_to_display = [] - # Only recalculate at every new-line, because this is sort of expensive - elif completion.endswith("\n"): - contents_lines = rif.contents.split("\n") - rewritten_lines = 0 - for line in lines: - for i in range(rewritten_lines, len(contents_lines)): - if difflib.SequenceMatcher(None, line, contents_lines[i]).ratio() > 0.7 and contents_lines[i].strip() != "": - rewritten_lines = i + 1 - break - lines_to_display = contents_lines[rewritten_lines:] - - new_file_contents = "\n".join( - full_prefix_lines) + "\n" + completion + "\n" + ("\n".join(lines_to_display) + "\n" if len(lines_to_display) > 0 else "") + "\n".join(full_suffix_lines) - - step_index = sdk.history.current_index - - await sdk.ide.showDiff(rif.filepath, new_file_contents, step_index) - - # Important state variables - # ------------------------- - original_lines = [] if rif.contents == "" else rif.contents.split("\n") - # In the actual file, taking into account block offset - current_line_in_file = rif.range.start.line - current_block_lines = [] - original_lines_below_previous_blocks = original_lines - # The start of the current block in file, taking into account block offset - current_block_start = -1 - offset_from_blocks = 0 - - # Don't end the block until you've matched N simultaneous lines - # This helps avoid many tiny blocks - LINES_TO_MATCH_BEFORE_ENDING_BLOCK = 2 - # If a line has been matched at the end of the block, this is its index within original_lines_below_previous_blocks - # Except we are keeping track of multiple potentialities, so it's a list - # We always check the lines following each of these leads, but if multiple make it out at the end, we use the first one - # This is a tuple of (index_of_last_matched_line, number_of_lines_matched) - indices_of_last_matched_lines = [] - - async def handle_generated_line(line: str): - nonlocal current_block_start, current_line_in_file, original_lines, original_lines_below_previous_blocks, current_block_lines, indices_of_last_matched_lines, LINES_TO_MATCH_BEFORE_ENDING_BLOCK, offset_from_blocks - - # Highlight the line to show progress - line_to_highlight = current_line_in_file - len(current_block_lines) - if False: - await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=Range.from_shorthand( - line_to_highlight, 0, line_to_highlight, 0)), "#FFFFFF22" if len(current_block_lines) == 0 else "#00FF0022") - - if len(current_block_lines) == 0: - # Set this as the start of the next block - current_block_start = rif.range.start.line + len(original_lines) - len( - original_lines_below_previous_blocks) + offset_from_blocks - if len(original_lines_below_previous_blocks) > 0 and line == original_lines_below_previous_blocks[0]: - # Line is equal to the next line in file, move past this line - original_lines_below_previous_blocks = original_lines_below_previous_blocks[ - 1:] - return - - # In a block, and have already matched at least one line - # Check if the next line matches, for each of the candidates - matches_found = [] - first_valid_match = None - for index_of_last_matched_line, num_lines_matched in indices_of_last_matched_lines: - if index_of_last_matched_line + 1 < len(original_lines_below_previous_blocks) and line == original_lines_below_previous_blocks[index_of_last_matched_line + 1]: - matches_found.append( - (index_of_last_matched_line + 1, num_lines_matched + 1)) - if first_valid_match is None and num_lines_matched + 1 >= LINES_TO_MATCH_BEFORE_ENDING_BLOCK: - first_valid_match = ( - index_of_last_matched_line + 1, num_lines_matched + 1) - indices_of_last_matched_lines = matches_found - - if first_valid_match is not None: - # We've matched the required number of lines, insert suggestion! - - # We added some lines to the block that were matched (including maybe some blank lines) - # So here we will strip all matching lines from the end of current_block_lines - lines_stripped = [] - index_of_last_line_in_block = first_valid_match[0] - while len(current_block_lines) > 0 and current_block_lines[-1] == original_lines_below_previous_blocks[index_of_last_line_in_block - 1]: - lines_stripped.append(current_block_lines.pop()) - index_of_last_line_in_block -= 1 - - # It's also possible that some lines match at the beginning of the block - # lines_stripped_at_beginning = [] - # j = 0 - # while len(current_block_lines) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[first_valid_match[0] - first_valid_match[1] + j]: - # lines_stripped_at_beginning.append( - # current_block_lines.pop(0)) - # j += 1 - # # current_block_start += 1 - - # Insert the suggestion - replacement = "\n".join(current_block_lines) - start_line = current_block_start - end_line = current_block_start + index_of_last_line_in_block - - if False: - await sdk.ide.showSuggestion(FileEdit( - filepath=rif.filepath, - range=Range.from_shorthand( - start_line, 0, end_line, 0), - replacement=replacement - )) - - # Reset current block / update variables - current_line_in_file += 1 - offset_from_blocks += len(current_block_lines) - original_lines_below_previous_blocks = original_lines_below_previous_blocks[ - index_of_last_line_in_block + 1:] - current_block_lines = [] - current_block_start = -1 - indices_of_last_matched_lines = [] - - return - - # Always look for new matching candidates - new_matches = [] - for i in range(len(original_lines_below_previous_blocks)): - og_line = original_lines_below_previous_blocks[i] - # TODO: It's a bit sus to be disqualifying empty lines. - # What you ideally do is find ALL matches, and then throw them out as you check the following lines - if og_line == line: # and og_line.strip() != "": - new_matches.append((i, 1)) - indices_of_last_matched_lines += new_matches - - # Make sure they are sorted by index - indices_of_last_matched_lines = sorted( - indices_of_last_matched_lines, key=lambda x: x[0]) - - current_block_lines.append(line) - - messages = await sdk.get_chat_context() - # Delete the last user and assistant messages - i = len(messages) - 1 - deleted = 0 - while i >= 0 and deleted < 2: - if messages[i].role == "user" or messages[i].role == "assistant": - messages.pop(i) - deleted += 1 - i -= 1 - messages.append(ChatMessage( - role="user", - content=prompt, - summary=self.user_input - )) - - lines_of_prefix_copied = 0 - lines = [] - unfinished_line = "" - completion_lines_covered = 0 - repeating_file_suffix = False - line_below_highlighted_range = file_suffix.lstrip().split("\n")[0] - - if isinstance(model_to_use, GGML): - messages = [ChatMessage( - role="user", content=f"```\n{rif.contents}\n```\n\nUser request: \"{self.user_input}\"\n\nThis is the code after changing to perfectly comply with the user request. It does not include any placeholder code, only real implementations:\n\n```\n", summary=self.user_input)] - - generator = model_to_use.stream_chat( - messages, temperature=sdk.config.temperature, max_tokens=max_tokens) - - try: - async for chunk in generator: - # Stop early if it is repeating the file_suffix or the step was deleted - if repeating_file_suffix: - break - if sdk.current_step_was_deleted(): - return - - # Accumulate lines - if "content" not in chunk: - continue - chunk = chunk["content"] - chunk_lines = chunk.split("\n") - chunk_lines[0] = unfinished_line + chunk_lines[0] - if chunk.endswith("\n"): - unfinished_line = "" - chunk_lines.pop() # because this will be an empty string - else: - unfinished_line = chunk_lines.pop() - - # Deal with newly accumulated lines - for i in range(len(chunk_lines)): - # Trailing whitespace doesn't matter - chunk_lines[i] = chunk_lines[i].rstrip() - chunk_lines[i] = common_whitespace + chunk_lines[i] - - # Lines that should signify the end of generation - if self.is_end_line(chunk_lines[i]): - break - # Lines that should be ignored, like the <> tags - elif self.line_to_be_ignored(chunk_lines[i], completion_lines_covered == 0): - continue - # Check if we are currently just copying the prefix - elif (lines_of_prefix_copied > 0 or completion_lines_covered == 0) and lines_of_prefix_copied < len(file_prefix.splitlines()) and chunk_lines[i] == full_file_contents_lines[lines_of_prefix_copied]: - # This is a sketchy way of stopping it from repeating the file_prefix. Is a bug if output happens to have a matching line - lines_of_prefix_copied += 1 - continue - # Because really short lines might be expected to be repeated, this is only a !heuristic! - # Stop when it starts copying the file_suffix - elif chunk_lines[i].strip() == line_below_highlighted_range.strip() and len(chunk_lines[i].strip()) > 4 and not (len(original_lines_below_previous_blocks) > 0 and chunk_lines[i].strip() == original_lines_below_previous_blocks[0].strip()): - repeating_file_suffix = True - break - - # If none of the above, insert the line! - if False: - await handle_generated_line(chunk_lines[i]) - - lines.append(chunk_lines[i]) - completion_lines_covered += 1 - current_line_in_file += 1 - - await sendDiffUpdate(lines + [common_whitespace if unfinished_line.startswith("<") else (common_whitespace + unfinished_line)], sdk) - finally: - await generator.aclose() - # Add the unfinished line - if unfinished_line != "" and not self.line_to_be_ignored(unfinished_line, completion_lines_covered == 0) and not self.is_end_line(unfinished_line): - unfinished_line = common_whitespace + unfinished_line - lines.append(unfinished_line) - await handle_generated_line(unfinished_line) - completion_lines_covered += 1 - current_line_in_file += 1 - - await sendDiffUpdate(lines, sdk, final=True) - - if False: - # If the current block isn't empty, add that suggestion - if len(current_block_lines) > 0: - # We have a chance to back-track here for blank lines that are repeats of the end of the original - # Don't want to have the same ending in both the original and the generated, can just leave it there - num_to_remove = 0 - for i in range(-1, -len(current_block_lines) - 1, -1): - if len(original_lines_below_previous_blocks) == 0: - break - if current_block_lines[i] == original_lines_below_previous_blocks[-1]: - num_to_remove += 1 - original_lines_below_previous_blocks.pop() - else: - break - current_block_lines = current_block_lines[:- - num_to_remove] if num_to_remove > 0 else current_block_lines - - # It's also possible that some lines match at the beginning of the block - # while len(current_block_lines) > 0 and len(original_lines_below_previous_blocks) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[0]: - # current_block_lines.pop(0) - # original_lines_below_previous_blocks.pop(0) - # current_block_start += 1 - - await sdk.ide.showSuggestion(FileEdit( - filepath=rif.filepath, - range=Range.from_shorthand( - current_block_start, 0, current_block_start + len(original_lines_below_previous_blocks), 0), - replacement="\n".join(current_block_lines) - )) - - # Record the completion - completion = "\n".join(lines) - self._previous_contents = "\n".join(original_lines) - self._new_contents = completion - self._prompt_and_completion += prompt + completion - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - await sdk.update_ui() - - rif_with_contents = [] - for range_in_file in map(lambda x: RangeInFile( - filepath=x.filepath, - # Only consider the range line-by-line. Maybe later don't if it's only a single line. - range=x.range.to_full_lines() - ), self.range_in_files): - file_contents = await sdk.ide.readRangeInFile(range_in_file) - rif_with_contents.append( - RangeInFileWithContents.from_range_in_file(range_in_file, file_contents)) - - rif_dict = {} - for rif in rif_with_contents: - rif_dict[rif.filepath] = rif.contents - - for rif in rif_with_contents: - # If the file doesn't exist, ask them to save it first - if not os.path.exists(rif.filepath): - message = f"The file {rif.filepath} does not exist. Please save it first." - raise ContinueCustomException( - title=message, message=message - ) - - await sdk.ide.setFileOpen(rif.filepath) - await sdk.ide.setSuggestionsLocked(rif.filepath, True) - await self.stream_rif(rif, sdk) - await sdk.ide.setSuggestionsLocked(rif.filepath, False) - - -class EditFileStep(Step): - filepath: str - prompt: str - hide: bool = True - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "Editing file: " + self.filepath - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - file_contents = await sdk.ide.readFile(self.filepath) - await sdk.run_step(DefaultModelEditCodeStep( - range_in_files=[RangeInFile.from_entire_file( - self.filepath, file_contents)], - user_input=self.prompt - )) - - -class ManualEditStep(ReversibleStep): - edit_diff: EditDiff - hide: bool = True - - hide: bool = True - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "Manual edit step" - # TODO - only handling FileEdit here, but need all other types of FileSystemEdits - # Also requires the merge_file_edit function - # return llm.complete(dedent(f"""This code was replaced: - - # {self.edit_diff.backward.replacement} - - # With this code: - - # {self.edit_diff.forward.replacement} - - # Maximally concise summary of changes in bullet points (can use markdown): - # """)) - - @classmethod - def from_sequence(cls, edits: List[FileEditWithFullContents]) -> "ManualEditStep": - diffs = [] - for edit in edits: - _, diff = FileSystem.apply_edit_to_str( - edit.fileContents, edit.fileEdit) - diffs.append(diff) - return cls(edit_diff=EditDiff.from_sequence(diffs)) - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - return None - - async def reverse(self, sdk: ContinueSDK): - await sdk.ide.applyFileSystemEdit(self.edit_diff.backward) - - -class UserInputStep(Step): - user_input: str - name: str = "User Input" - hide: bool = False - - manage_own_chat_context: bool = True - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return self.user_input - - async def run(self, sdk: ContinueSDK) -> Coroutine[UserInputObservation, None, None]: - self.chat_context.append(ChatMessage( - role="user", - content=self.user_input, - summary=self.user_input - )) - return UserInputObservation(user_input=self.user_input) - - -class WaitForUserInputStep(Step): - prompt: str - name: str = "Waiting for user input" - - _description: Union[str, None] = None - _response: Union[str, None] = None - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - if self._response is None: - return self.prompt - else: - return f"{self.prompt}\n\n`{self._response}`" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - self.description = self.prompt - resp = await sdk.wait_for_user_input() - self.description = f"{self.prompt}\n\n`{resp}`" - return TextObservation(text=resp) - - -class WaitForUserConfirmationStep(Step): - prompt: str - name: str = "Waiting for user confirmation" - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return self.prompt - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - self.description = self.prompt - resp = await sdk.wait_for_user_input() - return TextObservation(text=resp) diff --git a/continuedev/src/continuedev/steps/custom_command.py b/continuedev/src/continuedev/steps/custom_command.py deleted file mode 100644 index d96ac8e2..00000000 --- a/continuedev/src/continuedev/steps/custom_command.py +++ /dev/null @@ -1,28 +0,0 @@ -from ..libs.util.templating import render_templated_string -from ..core.main import Step -from ..core.sdk import ContinueSDK -from ..steps.chat import SimpleChatStep - - -class CustomCommandStep(Step): - name: str - prompt: str - user_input: str - slash_command: str - hide: bool = True - - async def describe(self): - return self.prompt - - async def run(self, sdk: ContinueSDK): - task = render_templated_string(self.prompt) - - prompt_user_input = f"Task: {task}. Additional info: {self.user_input}" - messages = await sdk.get_chat_context() - # Find the last chat message with this slash command and replace it with the user input - for i in range(len(messages) - 1, -1, -1): - if messages[i].role == "user" and messages[i].content.startswith(self.slash_command): - messages[i] = messages[i].copy( - update={"content": prompt_user_input}) - break - await sdk.run_step(SimpleChatStep(messages=messages)) diff --git a/continuedev/src/continuedev/steps/draft/abstract_method.py b/continuedev/src/continuedev/steps/draft/abstract_method.py deleted file mode 100644 index f3131c4b..00000000 --- a/continuedev/src/continuedev/steps/draft/abstract_method.py +++ /dev/null @@ -1,19 +0,0 @@ -from ....core.sdk import ContinueSDK -from ....core.main import Step - - -class ImplementAbstractMethodStep(Step): - name: str = "Implement abstract method for all subclasses" - method_name: str - class_name: str - - async def run(self, sdk: ContinueSDK): - - implementations = await sdk.lsp.go_to_implementations(self.class_name) - - for implementation in implementations: - - await sdk.edit_file( - range_in_files=[implementation.range_in_file], - prompt=f"Implement method `{self.method_name}` for this subclass of `{self.class_name}`", - ) diff --git a/continuedev/src/continuedev/steps/draft/migration.py b/continuedev/src/continuedev/steps/draft/migration.py deleted file mode 100644 index f3b36b5e..00000000 --- a/continuedev/src/continuedev/steps/draft/migration.py +++ /dev/null @@ -1,30 +0,0 @@ -# When an edit is made to an existing class or a new sqlalchemy class is created, -# this should be kicked off. - -from ...core.main import Step - - -class MigrationStep(Step): - name: str = "Create and run an alembic migration." - - edited_file: str - - async def run(self, sdk): - recent_edits = await sdk.ide.get_recent_edits(self.edited_file) - recent_edits_string = "\n\n".join( - map(lambda x: x.to_string(), recent_edits)) - description = await sdk.models.gpt35.complete(f"{recent_edits_string}\n\nGenerate a short description of the migration made in the above changes:\n") - await sdk.run([ - "cd libs", - "poetry run alembic revision --autogenerate -m " + description, - ]) - migration_file = f"libs/alembic/versions/{?}.py" - contents = await sdk.ide.readFile(migration_file) - await sdk.run_step(EditCodeStep( - range_in_files=[RangeInFile.from_entire_file(migration_file, contents)], - prompt=f"Here are the changes made to the sqlalchemy classes:\n\n{recent_edits_string}\n\nThis is the generated migration file:\n\n{{code}}\n\nReview the migration file to make sure it correctly reflects the changes made to the sqlalchemy classes.", - )) - await sdk.run([ - "cd libs", - "poetry run alembic upgrade head", - ]) diff --git a/continuedev/src/continuedev/steps/draft/redux.py b/continuedev/src/continuedev/steps/draft/redux.py deleted file mode 100644 index 17506316..00000000 --- a/continuedev/src/continuedev/steps/draft/redux.py +++ /dev/null @@ -1,47 +0,0 @@ -from ...core.main import Step -from ...core.sdk import ContinueSDK -from ..core.core import EditFileStep - - -class EditReduxStateStep(Step): - - description: str # e.g. "I want to load data from the weatherapi.com API" - - async def run(self, sdk: ContinueSDK): - # Find the right file to edit - - # RootStore - store_filename = "" - sdk.run_step( - EditFileStep( - filename=store_filename, - prompt=f"Edit the root store to add a new slice for {self.description}" - ) - ) - store_file_contents = await sdk.ide.readFile(store_filename) - - # Selector - selector_filename = "" - sdk.run_step(EditFileStep( - filepath=selector_filename, - prompt=f"Edit the selector to add a new property for {self.description}. The store looks like this: {store_file_contents}" - ) - - # Reducer - reducer_filename = "" - sdk.run_step(EditFileStep( - filepath=reducer_filename, - prompt=f"Edit the reducer to add a new property for {self.description}. The store looks like this: {store_file_contents}" - - """ - Starts with implementing selector - 1. RootStore - 2. Selector - 3. Reducer or entire slice - - Need to first determine whether this is an: - 1. edit - 2. add new reducer and property in existing slice - 3. add whole new slice - 4. build redux from scratch - """ diff --git a/continuedev/src/continuedev/steps/draft/typeorm.py b/continuedev/src/continuedev/steps/draft/typeorm.py deleted file mode 100644 index 153c855f..00000000 --- a/continuedev/src/continuedev/steps/draft/typeorm.py +++ /dev/null @@ -1,43 +0,0 @@ -from textwrap import dedent -from ...core.main import Step -from ...core.sdk import ContinueSDK - - -class CreateTableStep(Step): - sql_str: str - name: str = "Create a table in TypeORM" - - async def run(self, sdk: ContinueSDK): - # Write TypeORM entity - entity_name = self.sql_str.split(" ")[2].capitalize() - await sdk.edit_file( - f"src/entity/{entity_name}.ts", - dedent(f"""\ - {self.sql_str} - - Write a TypeORM entity called {entity_name} for this table, importing as necessary:""") - ) - - # Add entity to data-source.ts - await sdk.edit_file(filepath="src/data-source.ts", prompt=f"Add the {entity_name} entity:") - - # Generate blank migration for the entity - out = await sdk.run(f"npx typeorm migration:create ./src/migration/Create{entity_name}Table") - migration_filepath = out.text.split(" ")[1] - - # Wait for user input - await sdk.wait_for_user_confirmation("Fill in the migration?") - - # Fill in the migration - await sdk.edit_file( - migration_filepath, - dedent(f"""\ - This is the table that was created: - - {self.sql_str} - - Fill in the migration for the table:"""), - ) - - # Run the migration - await sdk.run("npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts") diff --git a/continuedev/src/continuedev/steps/feedback.py b/continuedev/src/continuedev/steps/feedback.py deleted file mode 100644 index 6f6a9b15..00000000 --- a/continuedev/src/continuedev/steps/feedback.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Coroutine -from ..core.main import Models -from ..core.main import Step -from ..core.sdk import ContinueSDK -from ..libs.util.telemetry import capture_event - - -class FeedbackStep(Step): - user_input: str - name = "Thanks for your feedback!" - - async def describe(self, models: Models): - return f"`{self.user_input}`\n\nWe'll see your feedback and make improvements as soon as possible. If you'd like to directly email us, you can contact [nate@continue.dev](mailto:nate@continue.dev?subject=Feedback%20On%20Continue)." - - async def run(self, sdk: ContinueSDK): - capture_event(sdk.ide.unique_id, "feedback", - {"feedback": self.user_input}) diff --git a/continuedev/src/continuedev/steps/find_and_replace.py b/continuedev/src/continuedev/steps/find_and_replace.py deleted file mode 100644 index 690872c0..00000000 --- a/continuedev/src/continuedev/steps/find_and_replace.py +++ /dev/null @@ -1,28 +0,0 @@ -from ..models.filesystem_edit import FileEdit, Range -from ..core.main import Models, Step -from ..core.sdk import ContinueSDK - - -class FindAndReplaceStep(Step): - name: str = "Find and replace" - filepath: str - pattern: str - replacement: str - - async def describe(self, models: Models): - return f"Replaced all instances of `{self.pattern}` with `{self.replacement}` in `{self.filepath}`" - - async def run(self, sdk: ContinueSDK): - file_content = await sdk.ide.readFile(self.filepath) - while self.pattern in file_content: - start_index = file_content.index(self.pattern) - end_index = start_index + len(self.pattern) - await sdk.ide.applyFileSystemEdit(FileEdit( - filepath=self.filepath, - range=Range.from_indices( - file_content, start_index, end_index - 1), - replacement=self.replacement - )) - file_content = file_content[:start_index] + \ - self.replacement + file_content[end_index:] - await sdk.ide.saveFile(self.filepath) diff --git a/continuedev/src/continuedev/steps/help.py b/continuedev/src/continuedev/steps/help.py deleted file mode 100644 index ba1e6087..00000000 --- a/continuedev/src/continuedev/steps/help.py +++ /dev/null @@ -1,59 +0,0 @@ -from textwrap import dedent -from ..core.main import ChatMessage, Step -from ..core.sdk import ContinueSDK -from ..libs.util.telemetry import capture_event - -help = dedent("""\ - Continue is an open-source coding autopilot. It is a VS Code extension that brings the power of ChatGPT to your IDE. - - It gathers context for you and stores your interactions automatically, so that you can avoid copy/paste now and benefit from a customized Large Language Model (LLM) later. - - Continue can be used to... - 1. Edit chunks of code with specific instructions (e.g. "/edit migrate this digital ocean terraform file into one that works for GCP") - 2. Get answers to questions without switching windows (e.g. "how do I find running process on port 8000?") - 3. Generate files from scratch (e.g. "/edit Create a Python CLI tool that uses the posthog api to get events from DAUs") - - You tell Continue to edit a specific section of code by highlighting it. If you highlight multiple code sections, then it will only edit the one with the purple glow around it. You can switch which one has the purple glow by clicking the paint brush. - - If you don't highlight any code, then Continue will insert at the location of your cursor. - - Continue passes all of the sections of code you highlight, the code above and below the to-be edited highlighted code section, and all previous steps above input box as context to the LLM. - - You can use cmd+m (Mac) / ctrl+m (Windows) to open Continue. You can use cmd+shift+e / ctrl+shift+e to open file Explorer. You can add your own OpenAI API key to VS Code Settings with `cmd+,` - - If Continue is stuck loading, try using `cmd+shift+p` to open the command palette, search "Reload Window", and then select it. This will reload VS Code and Continue and often fixes issues. - - If you have feedback, please use /feedback to let us know how you would like to use Continue. We are excited to hear from you!""") - - -class HelpStep(Step): - - name: str = "Help" - user_input: str - manage_own_chat_context: bool = True - description: str = "" - - async def run(self, sdk: ContinueSDK): - - question = self.user_input - - prompt = dedent(f"""Please us the information below to provide a succinct answer to the following quesiton: {question} - - Information: - - {help}""") - - self.chat_context.append(ChatMessage( - role="user", - content=prompt, - summary="Help" - )) - messages = await sdk.get_chat_context() - generator = sdk.models.gpt4.stream_chat(messages) - async for chunk in generator: - if "content" in chunk: - self.description += chunk["content"] - await sdk.update_ui() - - capture_event(sdk.ide.unique_id, "help", { - "question": question, "answer": self.description}) diff --git a/continuedev/src/continuedev/steps/input/nl_multiselect.py b/continuedev/src/continuedev/steps/input/nl_multiselect.py deleted file mode 100644 index aee22866..00000000 --- a/continuedev/src/continuedev/steps/input/nl_multiselect.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Union -from ..core.core import WaitForUserInputStep -from ...core.main import Step -from ...core.sdk import ContinueSDK - - -class NLMultiselectStep(Step): - hide: bool = True - - prompt: str - options: List[str] - - async def run(self, sdk: ContinueSDK): - user_response = (await sdk.run_step(WaitForUserInputStep(prompt=self.prompt))).text - - def extract_option(text: str) -> Union[str, None]: - for option in self.options: - if option in text: - return option - return None - - first_try = extract_option(user_response.lower()) - if first_try is not None: - return first_try - - gpt_parsed = await sdk.models.gpt35.complete( - f"These are the available options are: [{', '.join(self.options)}]. The user requested {user_response}. This is the exact string from the options array that they selected:") - return extract_option(gpt_parsed) or self.options[0] diff --git a/continuedev/src/continuedev/steps/main.py b/continuedev/src/continuedev/steps/main.py deleted file mode 100644 index ce7cbc60..00000000 --- a/continuedev/src/continuedev/steps/main.py +++ /dev/null @@ -1,318 +0,0 @@ -import os -from typing import Coroutine, List, Union - -from pydantic import BaseModel, Field - -from ..libs.llm import LLM -from ..models.main import Traceback, Range -from ..models.filesystem_edit import EditDiff, FileEdit -from ..models.filesystem import RangeInFile, RangeInFileWithContents -from ..core.observation import Observation, TextObservation, TracebackObservation -from ..libs.llm.prompt_utils import MarkdownStyleEncoderDecoder -from textwrap import dedent -from ..core.main import ContinueCustomException, Step -from ..core.sdk import ContinueSDK, Models -from ..core.observation import Observation -import subprocess -from .core.core import DefaultModelEditCodeStep -from ..libs.util.calculate_diff import calculate_diff2 - - -class SetupContinueWorkspaceStep(Step): - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "Set up Continue workspace by adding a .continue directory" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - if not os.path.exists(os.path.join(await sdk.ide.getWorkspaceDirectory(), ".continue")): - await sdk.add_directory(".continue") - if not os.path.exists(os.path.join(await sdk.ide.getWorkspaceDirectory(), ".continue", "config.json")): - await sdk.add_file(".continue/config.json", dedent("""\ - { - "allow_anonymous_telemetry": true - }""")) - - -class Policy(BaseModel): - pass - - -class RunPolicyUntilDoneStep(Step): - policy: "Policy" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - next_step = self.policy.next(sdk.config, sdk.history) - while next_step is not None: - observation = await sdk.run_step(next_step) - next_step = self.policy.next(sdk.config, sdk.history) - return observation - - -class FasterEditHighlightedCodeStep(Step): - user_input: str - hide = True - _completion: str = "Edit Code" - _edit_diffs: Union[List[EditDiff], None] = None - _prompt: str = dedent("""\ - You will be given code to edit in order to perfectly satisfy the user request. All the changes you make must be described as replacements, which you should format in the following way: - FILEPATH - - REPLACE_ME - - REPLACE_WITH - - - where and can be multiple lines, but should be the mininum needed to make the edit. Be sure to maintain existing whitespace at the start of lines. - - For example, if you want to replace the code `x = 1` with `x = 2` in main.py, you would write: - FILEPATH - main.py - REPLACE_ME - x = 1 - REPLACE_WITH - x = 2 - If you wanted to delete the code - ``` - def sum(a, b): - return a + b - ``` - in main.py, you would write: - FILEPATH - main.py - REPLACE_ME - def sum(a, b): - return a + b - REPLACE_WITH - - You may need to make multiple edits; respond with exactly as many as needed. - - Below is the code before changes: - - {code} - - This is the user request: "{user_input}" - Here is the description of changes to make: -""") - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "Editing highlighted code" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - range_in_files = await sdk.get_code_context(only_editing=True) - if len(range_in_files) == 0: - # Get the full contents of all visible files - files = await sdk.ide.getVisibleFiles() - contents = {} - for file in files: - contents[file] = await sdk.ide.readFile(file) - - range_in_files = [RangeInFileWithContents.from_entire_file( - filepath, content) for filepath, content in contents.items()] - - enc_dec = MarkdownStyleEncoderDecoder(range_in_files) - code_string = enc_dec.encode() - prompt = self._prompt.format( - code=code_string, user_input=self.user_input) - - rif_dict = {} - for rif in range_in_files: - rif_dict[rif.filepath] = rif.contents - - completion = await sdk.models.gpt35.complete(prompt) - - # Temporarily doing this to generate description. - self._prompt = prompt - self._completion = completion - print(completion) - - # ALTERNATIVE DECODING STEP HERE - raw_file_edits = [] - lines = completion.split("\n") - current_edit = {} - status = "FILEPATH" - for i in range(0, len(lines)): - line = lines[i] - if line == "FILEPATH": - if "FILEPATH" in current_edit: - raw_file_edits.append(current_edit) - current_edit = {} - status = "FILEPATH" - elif line == "REPLACE_ME": - status = "REPLACE_ME" - elif line == "REPLACE_WITH": - status = "REPLACE_WITH" - elif status == "FILEPATH": - current_edit["filepath"] = line - elif status == "REPLACE_ME": - if "replace_me" in current_edit: - current_edit["replace_me"] += "\n" + line - else: - current_edit["replace_me"] = line - elif status == "REPLACE_WITH": - if "replace_with" in current_edit: - current_edit["replace_with"] += "\n" + line - else: - current_edit["replace_with"] = line - if "filepath" in current_edit: - raw_file_edits.append(current_edit) - - file_edits = [] - for edit in raw_file_edits: - filepath = edit["filepath"] - replace_me = edit["replace_me"] - replace_with = edit["replace_with"] - file_edits.append( - FileEdit(filepath=filepath, range=Range.from_lines_snippet_in_file(content=rif_dict[filepath], snippet=replace_me), replacement=replace_with)) - # ------------------------------ - - self._edit_diffs = [] - for file_edit in file_edits: - diff = await sdk.apply_filesystem_edit(file_edit) - self._edit_diffs.append(diff) - - for filepath in set([file_edit.filepath for file_edit in file_edits]): - await sdk.ide.saveFile(filepath) - await sdk.ide.setFileOpen(filepath) - - return None - - -class StarCoderEditHighlightedCodeStep(Step): - user_input: str - name: str = "Editing Code" - hide = False - _prompt: str = "{code}{user_request}" - - _prompt_and_completion: str = "" - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return await models.gpt35.complete(f"{self._prompt_and_completion}\n\nPlease give brief a description of the changes made above using markdown bullet points:") - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - range_in_files = await sdk.get_code_context(only_editing=True) - found_highlighted_code = len(range_in_files) > 0 - if not found_highlighted_code: - # Get the full contents of all visible files - files = await sdk.ide.getVisibleFiles() - contents = {} - for file in files: - contents[file] = await sdk.ide.readFile(file) - - range_in_files = [RangeInFileWithContents.from_entire_file( - filepath, content) for filepath, content in contents.items()] - - rif_dict = {} - for rif in range_in_files: - rif_dict[rif.filepath] = rif.contents - - for rif in range_in_files: - prompt = self._prompt.format( - code=rif.contents, user_request=self.user_input) - - if found_highlighted_code: - full_file_contents = await sdk.ide.readFile(rif.filepath) - segs = full_file_contents.split(rif.contents) - prompt = f"{segs[0]}{segs[1]}" + prompt - - completion = str(await sdk.models.starcoder.complete(prompt)) - eot_token = "<|endoftext|>" - completion = completion.removesuffix(eot_token) - - if found_highlighted_code: - rif.contents = segs[0] + rif.contents + segs[1] - completion = segs[0] + completion + segs[1] - - self._prompt_and_completion += prompt + completion - - edits = calculate_diff2( - rif.filepath, rif.contents, completion.removesuffix("\n")) - for edit in edits: - await sdk.ide.applyFileSystemEdit(edit) - - # await sdk.ide.applyFileSystemEdit( - # FileEdit(filepath=rif.filepath, range=rif.range, replacement=completion)) - await sdk.ide.saveFile(rif.filepath) - await sdk.ide.setFileOpen(rif.filepath) - - -class EditHighlightedCodeStep(Step): - user_input: str = Field( - ..., title="User Input", description="The natural language request describing how to edit the code") - hide = True - description: str = "Change the contents of the currently highlighted code or open file. You should call this function if the user asks seems to be asking for a code change." - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "Editing code" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - range_in_files = sdk.get_code_context(only_editing=True) - - # If nothing highlighted, insert at the cursor if possible - if len(range_in_files) == 0: - highlighted_code = await sdk.ide.getHighlightedCode() - if highlighted_code is not None: - for rif in highlighted_code: - if os.path.dirname(rif.filepath) == os.path.expanduser(os.path.join("~", ".continue", "diffs")): - raise ContinueCustomException( - message="Please accept or reject the change before making another edit in this file.", title="Accept/Reject First") - if rif.range.start == rif.range.end: - range_in_files.append( - RangeInFileWithContents.from_range_in_file(rif, "")) - - # If still no highlighted code, raise error - if len(range_in_files) == 0: - raise ContinueCustomException( - message="Please highlight some code and try again.", title="No Code Selected") - - range_in_files = list(map(lambda x: RangeInFile( - filepath=x.filepath, range=x.range - ), range_in_files)) - - for range_in_file in range_in_files: - if os.path.dirname(range_in_file.filepath) == os.path.expanduser(os.path.join("~", ".continue", "diffs")): - self.description = "Please accept or reject the change before making another edit in this file." - return - - await sdk.run_step(DefaultModelEditCodeStep(user_input=self.user_input, range_in_files=range_in_files)) - - -class UserInputStep(Step): - user_input: str - - -class SolveTracebackStep(Step): - traceback: Traceback - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return f"```\n{self.traceback.full_traceback}\n```" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - prompt = dedent("""I ran into this problem with my Python code: - - {traceback} - - Below are the files that might need to be fixed: - - {code} - - This is what the code should be in order to avoid the problem: - """).format(traceback=self.traceback.full_traceback, code="{code}") - - range_in_files = [] - for frame in self.traceback.frames: - content = await sdk.ide.readFile(frame.filepath) - range_in_files.append( - RangeInFile.from_entire_file(frame.filepath, content)) - - await sdk.run_step(EditCodeStep( - range_in_files=range_in_files, prompt=prompt)) - return None - - -class EmptyStep(Step): - hide: bool = True - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return "" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - pass diff --git a/continuedev/src/continuedev/steps/on_traceback.py b/continuedev/src/continuedev/steps/on_traceback.py deleted file mode 100644 index efb4c703..00000000 --- a/continuedev/src/continuedev/steps/on_traceback.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -from .core.core import UserInputStep -from ..core.main import ChatMessage, Step -from ..core.sdk import ContinueSDK -from .chat import SimpleChatStep - - -class DefaultOnTracebackStep(Step): - output: str - name: str = "Help With Traceback" - hide: bool = True - - async def run(self, sdk: ContinueSDK): - # Add context for any files in the traceback that are in the workspace - for line in self.output.split("\n"): - segs = line.split(" ") - for seg in segs: - if seg.startswith(os.path.sep) and os.path.exists(seg) and os.path.commonprefix([seg, sdk.ide.workspace_directory]) == sdk.ide.workspace_directory: - file_contents = await sdk.ide.readFile(seg) - self.chat_context.append(ChatMessage( - role="user", - content=f"The contents of {seg}:\n```\n{file_contents}\n```", - summary="" - )) - await sdk.run_step(UserInputStep(user_input=f"""I got the following error, can you please help explain how to fix it?\n\n{self.output}""")) - await sdk.run_step(SimpleChatStep(name="Help With Traceback")) diff --git a/continuedev/src/continuedev/steps/open_config.py b/continuedev/src/continuedev/steps/open_config.py deleted file mode 100644 index af55a95a..00000000 --- a/continuedev/src/continuedev/steps/open_config.py +++ /dev/null @@ -1,29 +0,0 @@ -from textwrap import dedent -from ..core.main import Step -from ..core.sdk import ContinueSDK -import os - - -class OpenConfigStep(Step): - name: str = "Open config" - - async def describe(self, models): - return dedent("""\ - `\"config.json\"` is now open. You can add a custom slash command in the `\"custom_commands\"` section, like in this example: - ```json - "custom_commands": [ - { - "name": "test", - "description": "Write unit tests like I do for the highlighted code", - "prompt": "Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated." - } - ] - ``` - `"name"` is the command you will type. - `"description"` is the description displayed in the slash command menu. - `"prompt"` is the instruction given to the model. The overall prompt becomes "Task: {prompt}, Additional info: {user_input}". For example, if you entered "/test exactly 5 assertions", the overall prompt would become "Task: Write a comprehensive...and sophisticated, Additional info: exactly 5 assertions".""") - - async def run(self, sdk: ContinueSDK): - global_dir = os.path.expanduser('~/.continue') - config_path = os.path.join(global_dir, 'config.json') - await sdk.ide.setFileOpen(config_path) diff --git a/continuedev/src/continuedev/steps/react.py b/continuedev/src/continuedev/steps/react.py deleted file mode 100644 index cddb8b42..00000000 --- a/continuedev/src/continuedev/steps/react.py +++ /dev/null @@ -1,43 +0,0 @@ -from textwrap import dedent -from typing import List, Union, Tuple -from ..core.main import Step -from ..core.sdk import ContinueSDK -from .core.core import MessageStep - - -class NLDecisionStep(Step): - user_input: str - default_step: Union[Step, None] = None - steps: List[Tuple[Step, str]] - - hide: bool = False - name: str = "Deciding what to do next" - - async def run(self, sdk: ContinueSDK): - step_descriptions = "\n".join([ - f"- {step[0].name}: {step[1]}" - for step in self.steps - ]) - prompt = dedent(f"""\ - The following steps are available, in the format "- [step name]: [step description]": - {step_descriptions} - - The user gave the following input: - - {self.user_input} - - Select the step which should be taken next to satisfy the user input. Say only the name of the selected step. You must choose one:""") - - resp = (await sdk.models.gpt35.complete(prompt)).lower() - - step_to_run = None - for step in self.steps: - if step[0].name.lower() in resp: - step_to_run = step[0] - - step_to_run = step_to_run or self.default_step or self.steps[0] - - self.hide = True - await sdk.update_ui() - - await sdk.run_step(step_to_run) diff --git a/continuedev/src/continuedev/steps/search_directory.py b/continuedev/src/continuedev/steps/search_directory.py deleted file mode 100644 index bfb97630..00000000 --- a/continuedev/src/continuedev/steps/search_directory.py +++ /dev/null @@ -1,69 +0,0 @@ -import asyncio -from textwrap import dedent -from typing import List, Union - -from ..models.filesystem import RangeInFile -from ..models.main import Range -from ..core.main import Step -from ..core.sdk import ContinueSDK -from ..libs.util.create_async_task import create_async_task -import os -import re - -# Already have some code for this somewhere -IGNORE_DIRS = ["env", "venv", ".venv"] -IGNORE_FILES = [".env"] - - -def find_all_matches_in_dir(pattern: str, dirpath: str) -> List[RangeInFile]: - range_in_files = [] - for root, dirs, files in os.walk(dirpath): - dirname = os.path.basename(root) - if dirname.startswith(".") or dirname in IGNORE_DIRS: - continue - for file in files: - if file in IGNORE_FILES: - continue - with open(os.path.join(root, file), "r") as f: - # Find the index of all occurences of the pattern in the file. Use re. - file_content = f.read() - results = re.finditer(pattern, file_content) - range_in_files += [ - RangeInFile(filepath=os.path.join(root, file), range=Range.from_indices( - file_content, result.start(), result.end())) - for result in results - ] - - return range_in_files - - -class WriteRegexPatternStep(Step): - user_request: str - - async def run(self, sdk: ContinueSDK): - # Ask the user for a regex pattern - pattern = await sdk.models.gpt35.complete(dedent(f"""\ - This is the user request: - - {self.user_request} - - Please write either a regex pattern or just a string that be used with python's re module to find all matches requested by the user. It will be used as `re.findall(, file_content)`. Your output should be only the regex or string, nothing else:""")) - - return pattern - - -class EditAllMatchesStep(Step): - pattern: str - user_request: str - directory: Union[str, None] = None - - async def run(self, sdk: ContinueSDK): - # Search all files for a given string - range_in_files = find_all_matches_in_dir(self.pattern, self.directory or await sdk.ide.getWorkspaceDirectory()) - - tasks = [create_async_task(sdk.edit_file( - range=range_in_file.range, - filename=range_in_file.filepath, - prompt=self.user_request - ), sdk.ide.unique_id) for range_in_file in range_in_files] - await asyncio.gather(*tasks) diff --git a/continuedev/src/continuedev/steps/steps_on_startup.py b/continuedev/src/continuedev/steps/steps_on_startup.py deleted file mode 100644 index 365cbe1a..00000000 --- a/continuedev/src/continuedev/steps/steps_on_startup.py +++ /dev/null @@ -1,23 +0,0 @@ -from ..core.main import Step -from ..core.sdk import Models, ContinueSDK -from .main import UserInputStep -from ..recipes.CreatePipelineRecipe.main import CreatePipelineRecipe -from ..recipes.DDtoBQRecipe.main import DDtoBQRecipe -from ..recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe -from ..recipes.DDtoBQRecipe.main import DDtoBQRecipe -from ..recipes.AddTransformRecipe.main import AddTransformRecipe -from ..libs.util.step_name_to_steps import get_step_from_name - - -class StepsOnStartupStep(Step): - hide: bool = True - - async def describe(self, models: Models): - return "Running steps on startup" - - async def run(self, sdk: ContinueSDK): - steps_on_startup = sdk.config.steps_on_startup - - for step_name, step_params in steps_on_startup.items(): - step = get_step_from_name(step_name, step_params) - await sdk.run_step(step) diff --git a/continuedev/src/continuedev/steps/welcome.py b/continuedev/src/continuedev/steps/welcome.py deleted file mode 100644 index 2dece649..00000000 --- a/continuedev/src/continuedev/steps/welcome.py +++ /dev/null @@ -1,32 +0,0 @@ -from textwrap import dedent -from ..models.filesystem_edit import AddFile -from ..core.main import Step -from ..core.sdk import ContinueSDK, Models -import os - - -class WelcomeStep(Step): - name: str = "Welcome to Continue!" - hide: bool = True - - async def describe(self, models: Models): - return "Welcome to Continue!" - - async def run(self, sdk: ContinueSDK): - continue_dir = os.path.expanduser("~/.continue") - filepath = os.path.join(continue_dir, "calculator.py") - if os.path.exists(filepath): - return - if not os.path.exists(continue_dir): - os.mkdir(continue_dir) - - await sdk.ide.applyFileSystemEdit(AddFile(filepath=filepath, content=dedent("""\ - \"\"\" - Welcome to Continue! To learn how to use it, delete this comment and try to use Continue for the following: - - "Write me a calculator class" - - Ask for a new method (e.g. "exp", "mod", "sqrt") - - Type /comment to write comments for the entire class - - Ask about how the class works, how to write it in another language, etc. - \"\"\""""))) - - # await sdk.ide.setFileOpen(filepath=filepath) -- cgit v1.2.3-70-g09d2