truefoundry 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of truefoundry might be problematic. Click here for more details.
- truefoundry/autodeploy/__init__.py +0 -0
- truefoundry/autodeploy/agents/__init__.py +0 -0
- truefoundry/autodeploy/agents/base.py +181 -0
- truefoundry/autodeploy/agents/developer.py +113 -0
- truefoundry/autodeploy/agents/project_identifier.py +124 -0
- truefoundry/autodeploy/agents/tester.py +75 -0
- truefoundry/autodeploy/cli.py +348 -0
- truefoundry/autodeploy/constants.py +22 -0
- truefoundry/autodeploy/exception.py +2 -0
- truefoundry/autodeploy/logger.py +13 -0
- truefoundry/autodeploy/tools/__init__.py +26 -0
- truefoundry/autodeploy/tools/ask.py +33 -0
- truefoundry/autodeploy/tools/base.py +31 -0
- truefoundry/autodeploy/tools/commit.py +139 -0
- truefoundry/autodeploy/tools/docker_build.py +109 -0
- truefoundry/autodeploy/tools/docker_run.py +150 -0
- truefoundry/autodeploy/tools/file_type_counts.py +79 -0
- truefoundry/autodeploy/tools/list_files.py +82 -0
- truefoundry/autodeploy/tools/read_file.py +66 -0
- truefoundry/autodeploy/tools/send_request.py +54 -0
- truefoundry/autodeploy/tools/write_file.py +101 -0
- truefoundry/autodeploy/utils/diff.py +157 -0
- truefoundry/autodeploy/utils/pydantic_compat.py +19 -0
- truefoundry/cli/__main__.py +11 -5
- truefoundry/deploy/__init__.py +1 -1
- truefoundry/deploy/cli/__init__.py +0 -0
- truefoundry/deploy/cli/cli.py +99 -0
- truefoundry/deploy/cli/deploy.py +184 -0
- truefoundry/langchain/__init__.py +1 -1
- truefoundry/ml/__init__.py +4 -2
- {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/METADATA +13 -5
- truefoundry-0.2.0.dist-info/RECORD +36 -0
- truefoundry-0.1.1.dist-info/RECORD +0 -10
- {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/WHEEL +0 -0
- {truefoundry-0.1.1.dist-info → truefoundry-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Generator, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
import docker
|
|
8
|
+
from docker.models.images import BuildError, json_stream
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
11
|
+
from rich.padding import Padding
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from truefoundry.autodeploy.tools.base import (
|
|
15
|
+
Event,
|
|
16
|
+
Message,
|
|
17
|
+
RequestEvent,
|
|
18
|
+
ResponseEvent,
|
|
19
|
+
Tool,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DockerBuildLog(Event):
|
|
24
|
+
log: str
|
|
25
|
+
|
|
26
|
+
def render(self, console: Console):
|
|
27
|
+
console.print(Padding.indent(renderable=Text.from_ansi(self.log), level=2))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# vendored from
|
|
31
|
+
# https://github.com/docker/docker-py/blob/9ad4bddc9ee23f3646f256280a21ef86274e39bc/docker/models/images.py#L220
|
|
32
|
+
def _build(docker_client: docker.DockerClient, **kwargs) -> Iterable[DockerBuildLog]:
|
|
33
|
+
resp = docker_client.images.client.api.build(**kwargs)
|
|
34
|
+
if isinstance(resp, str):
|
|
35
|
+
return docker_client.images.get(resp)
|
|
36
|
+
last_event = None
|
|
37
|
+
image_id = None
|
|
38
|
+
result_stream, internal_stream = itertools.tee(json_stream(resp))
|
|
39
|
+
for chunk in internal_stream:
|
|
40
|
+
if "error" in chunk:
|
|
41
|
+
raise BuildError(chunk["error"], result_stream)
|
|
42
|
+
if "stream" in chunk:
|
|
43
|
+
yield DockerBuildLog(log=chunk["stream"])
|
|
44
|
+
match = re.search(
|
|
45
|
+
r"(^Successfully built |sha256:)([0-9a-f]+)$", chunk["stream"]
|
|
46
|
+
)
|
|
47
|
+
if match:
|
|
48
|
+
image_id = match.group(2)
|
|
49
|
+
last_event = chunk
|
|
50
|
+
if image_id:
|
|
51
|
+
return None
|
|
52
|
+
raise BuildError(last_event or "Unknown", result_stream)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DockerBuild(Tool):
|
|
56
|
+
description = """
|
|
57
|
+
Build a docker image.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
class Request(RequestEvent):
|
|
61
|
+
dockerfile_path: str = Field(
|
|
62
|
+
...,
|
|
63
|
+
pattern=r"^[a-zA-Z0-9\.]{1}.*$",
|
|
64
|
+
description="Dockerfile path. ",
|
|
65
|
+
)
|
|
66
|
+
image_tag: str = Field(..., description="image tag")
|
|
67
|
+
|
|
68
|
+
class Response(ResponseEvent):
|
|
69
|
+
error: Optional[str] = Field(None, description="Error raised while building")
|
|
70
|
+
build_logs: Optional[str] = Field(None, description="Build logs")
|
|
71
|
+
|
|
72
|
+
def __rich_console__(
|
|
73
|
+
self, console: Console, options: ConsoleOptions
|
|
74
|
+
) -> RenderResult:
|
|
75
|
+
none_text = "[italic magenta]None[/]"
|
|
76
|
+
error_text = (
|
|
77
|
+
f"[green]'{self.error}'[/]" if self.error is not None else none_text
|
|
78
|
+
)
|
|
79
|
+
yield Text.from_markup("[bold magenta]Response[/](")
|
|
80
|
+
if self.build_logs is not None:
|
|
81
|
+
yield Text.from_markup(' [yellow]build_logs[/]= "')
|
|
82
|
+
yield Text.from_ansi(self.build_logs)
|
|
83
|
+
yield Text.from_markup('"')
|
|
84
|
+
else:
|
|
85
|
+
yield Text.from_markup(f" [yellow]build_logs[/]={none_text}")
|
|
86
|
+
yield Text.from_markup(f" [yellow]error[/]={error_text}\n)")
|
|
87
|
+
|
|
88
|
+
def __init__(self, project_root_path: str, docker_client: docker.DockerClient):
|
|
89
|
+
self.project_root_path = project_root_path
|
|
90
|
+
self.docker_client = docker_client
|
|
91
|
+
|
|
92
|
+
def run(self, request: DockerBuild.Request) -> Generator[Event, Any, ResponseEvent]:
|
|
93
|
+
yield Message(message="[bold cyan]Processing:[/] Building Docker image...")
|
|
94
|
+
yield Message(message="[bold yellow]Docker build logs:[/]")
|
|
95
|
+
try:
|
|
96
|
+
for message in _build(
|
|
97
|
+
self.docker_client,
|
|
98
|
+
path=self.project_root_path,
|
|
99
|
+
tag=request.image_tag,
|
|
100
|
+
):
|
|
101
|
+
yield message
|
|
102
|
+
return DockerBuild.Response()
|
|
103
|
+
except BuildError as ex:
|
|
104
|
+
logs = ""
|
|
105
|
+
for log_line in ex.build_log:
|
|
106
|
+
logs += log_line.get("stream", "")
|
|
107
|
+
return DockerBuild.Response(error=str(ex), build_logs=logs[-800:])
|
|
108
|
+
except (docker.errors.APIError, docker.errors.DockerException) as ex:
|
|
109
|
+
return DockerBuild.Response(error=str(ex), build_logs="")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
from typing import Any, Dict, Generator, List, Optional
|
|
5
|
+
|
|
6
|
+
import docker
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
10
|
+
from rich.padding import Padding
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from truefoundry.autodeploy.tools.base import (
|
|
14
|
+
Event,
|
|
15
|
+
Message,
|
|
16
|
+
RequestEvent,
|
|
17
|
+
ResponseEvent,
|
|
18
|
+
Tool,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DockerRunLog(Event):
|
|
23
|
+
index: int
|
|
24
|
+
log: str
|
|
25
|
+
|
|
26
|
+
def render(self, console: Console):
|
|
27
|
+
# if self.index > 1:
|
|
28
|
+
# print("\r", end="")
|
|
29
|
+
console.print(Padding.indent(renderable=Text.from_ansi(self.log), level=2))
|
|
30
|
+
# console.print("Press control-c to stop waiting for more logs", end="")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DockerRun(Tool):
|
|
34
|
+
description = """
|
|
35
|
+
Run a docker image
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
class Request(RequestEvent):
|
|
39
|
+
image_tag: str
|
|
40
|
+
ports: Optional[Dict[str, int]] = Field(
|
|
41
|
+
None,
|
|
42
|
+
description="""
|
|
43
|
+
Ports to expose.
|
|
44
|
+
The keys of the dictionary are the ports to bind inside the container in 'port'.
|
|
45
|
+
The values are the ports to open on the host""",
|
|
46
|
+
)
|
|
47
|
+
command: str
|
|
48
|
+
|
|
49
|
+
def render(self, console: Console):
|
|
50
|
+
console.print(
|
|
51
|
+
f"[bold magenta]TrueFoundry[/] is executing the Docker container. Image Tag: [bold green]{self.image_tag}[/], Exposed Port: [bold green]{str(self.ports) if self.ports is not None else 'Not exposed'}[/], Command: [bold green]{self.command}[/]"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class Response(ResponseEvent):
|
|
55
|
+
logs: Optional[List[str]] = Field(
|
|
56
|
+
None, description="Logs of the container. Only last 50 chars."
|
|
57
|
+
)
|
|
58
|
+
exit_code: Optional[int] = Field(
|
|
59
|
+
None,
|
|
60
|
+
description="""
|
|
61
|
+
Exit code of the process if the container stops.
|
|
62
|
+
This will not be passed if the container is still running.
|
|
63
|
+
""",
|
|
64
|
+
)
|
|
65
|
+
client_error: Optional[str] = Field(None, description="Docker client error.")
|
|
66
|
+
|
|
67
|
+
def __rich_console__(
|
|
68
|
+
self, console: Console, options: ConsoleOptions
|
|
69
|
+
) -> RenderResult:
|
|
70
|
+
none_text = "[italic magenta]None[/]"
|
|
71
|
+
error_text = (
|
|
72
|
+
f"[green]'{self.client_error}'[/]"
|
|
73
|
+
if self.client_error is not None
|
|
74
|
+
else none_text
|
|
75
|
+
)
|
|
76
|
+
exit_code_text = (
|
|
77
|
+
"[green]'0'[/]"
|
|
78
|
+
if self.exit_code == 0
|
|
79
|
+
else (
|
|
80
|
+
none_text if self.exit_code is None else f"[red]{self.exit_code}[/]"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
yield Text.from_markup("[bold magenta]Response[/](")
|
|
85
|
+
if self.logs is not None:
|
|
86
|
+
yield Text.from_markup(' [yellow]logs=[/]"')
|
|
87
|
+
yield Text.from_ansi("".join(self.logs))
|
|
88
|
+
yield Text.from_markup('"')
|
|
89
|
+
else:
|
|
90
|
+
yield Text.from_markup(f" [yellow]logs[/]={none_text}")
|
|
91
|
+
|
|
92
|
+
yield Text.from_markup(f" [yellow]exit_code[/]={exit_code_text}")
|
|
93
|
+
yield Text.from_markup(f" [yellow]client_error[/]={error_text} \n)")
|
|
94
|
+
|
|
95
|
+
def __init__(self, docker_client: docker.DockerClient, environment: Dict):
|
|
96
|
+
self.containers = []
|
|
97
|
+
self.docker_client = docker_client
|
|
98
|
+
self.environment = environment
|
|
99
|
+
atexit.register(self._kill_running_containers)
|
|
100
|
+
|
|
101
|
+
def _kill_running_containers(self):
|
|
102
|
+
if self.containers:
|
|
103
|
+
container = self.containers.pop()
|
|
104
|
+
try:
|
|
105
|
+
container.remove(force=True)
|
|
106
|
+
except docker.errors.APIError:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def run(self, request: DockerRun.Request) -> Generator[Event, Any, ResponseEvent]:
|
|
110
|
+
self._kill_running_containers()
|
|
111
|
+
yield Message(message="[bold cyan]Testing:[/] Running Docker container...")
|
|
112
|
+
try:
|
|
113
|
+
container = self.docker_client.containers.run(
|
|
114
|
+
request.image_tag,
|
|
115
|
+
detach=True,
|
|
116
|
+
remove=False,
|
|
117
|
+
stderr=True,
|
|
118
|
+
ports=request.ports,
|
|
119
|
+
environment=self.environment,
|
|
120
|
+
command=request.command,
|
|
121
|
+
)
|
|
122
|
+
except docker.errors.APIError as ex:
|
|
123
|
+
if ex.is_client_error():
|
|
124
|
+
return DockerRun.Response(client_error=str(ex))
|
|
125
|
+
raise
|
|
126
|
+
yield Message(message="[bold yellow]Docker logs:[/]")
|
|
127
|
+
self.containers.append(container)
|
|
128
|
+
exit_code = None
|
|
129
|
+
|
|
130
|
+
all_logs = []
|
|
131
|
+
logs = container.logs(stream=True)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
for i, log in enumerate(logs):
|
|
135
|
+
log = log.decode()
|
|
136
|
+
all_logs.append(log)
|
|
137
|
+
yield DockerRunLog(index=i, log=log)
|
|
138
|
+
except KeyboardInterrupt:
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
yield Message(message="\n[bold yellow]There are no more logs.[/]")
|
|
142
|
+
exit_code = None
|
|
143
|
+
try:
|
|
144
|
+
exit_code = container.wait(timeout=1).get("StatusCode")
|
|
145
|
+
except (
|
|
146
|
+
requests.exceptions.ReadTimeout,
|
|
147
|
+
requests.exceptions.ConnectionError,
|
|
148
|
+
):
|
|
149
|
+
...
|
|
150
|
+
return DockerRun.Response(logs=all_logs, exit_code=exit_code)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from typing import Any, Dict, Generator
|
|
6
|
+
|
|
7
|
+
import gitignorefile
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from truefoundry.autodeploy.tools.base import (
|
|
13
|
+
Event,
|
|
14
|
+
Message,
|
|
15
|
+
RequestEvent,
|
|
16
|
+
ResponseEvent,
|
|
17
|
+
Tool,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShowFileCount(Event):
|
|
22
|
+
file_types: Dict[str, int]
|
|
23
|
+
|
|
24
|
+
def render(self, console: Console):
|
|
25
|
+
total_files = sum(self.file_types.values())
|
|
26
|
+
console.print(f"Found {total_files} files.")
|
|
27
|
+
table = Table()
|
|
28
|
+
table.add_column("File Type", style="cyan")
|
|
29
|
+
table.add_column("Count", justify="right", style="green")
|
|
30
|
+
for file_type, count in self.file_types.items():
|
|
31
|
+
table.add_row(file_type, str(count))
|
|
32
|
+
|
|
33
|
+
console.print(table)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FileTypeCounts(Tool):
|
|
37
|
+
description = """
|
|
38
|
+
Get counts of different types of file present.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
class Request(RequestEvent): ...
|
|
42
|
+
|
|
43
|
+
class Response(ResponseEvent):
|
|
44
|
+
file_types: Dict[str, int] = Field(
|
|
45
|
+
...,
|
|
46
|
+
description='Counts of different types of files. Ex: {"py": 1} or {"c": 1}',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(self, project_root_path: str):
|
|
50
|
+
self.project_root_path = project_root_path
|
|
51
|
+
|
|
52
|
+
def run(
|
|
53
|
+
self, request: FileTypeCounts.Request
|
|
54
|
+
) -> Generator[Event, Any, ResponseEvent]:
|
|
55
|
+
counter = Counter()
|
|
56
|
+
|
|
57
|
+
yield Message(
|
|
58
|
+
message="[bold cyan]Processing:[/] Scanning for various file types..."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def gitignore(_):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
gitignore_path = os.path.join(self.project_root_path, ".gitignore")
|
|
65
|
+
if os.path.exists(gitignore_path):
|
|
66
|
+
gitignore = gitignorefile.parse(path=gitignore_path)
|
|
67
|
+
for root, dirs, ps in os.walk(
|
|
68
|
+
self.project_root_path,
|
|
69
|
+
):
|
|
70
|
+
root = root[len(self.project_root_path) :]
|
|
71
|
+
if ".git" in dirs:
|
|
72
|
+
dirs.remove(".git")
|
|
73
|
+
counter.update(
|
|
74
|
+
p.split(".")[-1] if len(p) and p[0] != "." else p
|
|
75
|
+
for p in ps
|
|
76
|
+
if not gitignore(os.path.join(root, p).strip(os.path.sep))
|
|
77
|
+
)
|
|
78
|
+
yield ShowFileCount(file_types=counter)
|
|
79
|
+
return FileTypeCounts.Response(file_types=counter)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
|
+
from typing import Any, Generator, List, Optional
|
|
6
|
+
|
|
7
|
+
import gitignorefile
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from truefoundry.autodeploy.tools.base import (
|
|
11
|
+
Event,
|
|
12
|
+
Message,
|
|
13
|
+
RequestEvent,
|
|
14
|
+
ResponseEvent,
|
|
15
|
+
Tool,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ListFiles(Tool):
|
|
20
|
+
description = """
|
|
21
|
+
List all files.
|
|
22
|
+
If you want to find all json files recuresively under directory a/b
|
|
23
|
+
the subdir should be a/b and pattern will be *.json
|
|
24
|
+
|
|
25
|
+
If you want to find all json files recuresively under current directory
|
|
26
|
+
the subdir should be . and pattern will be *.json
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
class Request(RequestEvent):
|
|
30
|
+
sub_dir: str
|
|
31
|
+
pattern: str = Field(..., description="Glob pattern. Avoid passing '*'")
|
|
32
|
+
|
|
33
|
+
class Response(ResponseEvent):
|
|
34
|
+
paths: List[str] = Field(
|
|
35
|
+
...,
|
|
36
|
+
description="File paths under the given directory",
|
|
37
|
+
)
|
|
38
|
+
error: Optional[str] = Field(None)
|
|
39
|
+
|
|
40
|
+
def __init__(self, project_root_path: str):
|
|
41
|
+
self.project_root_path = project_root_path
|
|
42
|
+
|
|
43
|
+
def run(self, request: ListFiles.Request) -> Generator[Event, Any, ResponseEvent]:
|
|
44
|
+
yield Message(
|
|
45
|
+
message=f"[bold cyan]Searching:[/] 🔍 Looking for files matching the pattern [magenta]{request.pattern}[/]"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
paths: List[str] = []
|
|
49
|
+
|
|
50
|
+
def gitignore(_):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
gitignore_path = os.path.join(self.project_root_path, ".gitignore")
|
|
54
|
+
if os.path.exists(gitignore_path):
|
|
55
|
+
gitignore = gitignorefile.parse(path=gitignore_path)
|
|
56
|
+
|
|
57
|
+
path = os.path.join(self.project_root_path, request.sub_dir.strip(os.path.sep))
|
|
58
|
+
if not os.path.exists(path):
|
|
59
|
+
return ListFiles.Response(
|
|
60
|
+
paths=None,
|
|
61
|
+
error=f"Incorrect sub_dir {request.sub_dir}. Does not exist",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
for root, dirs, ps in os.walk(
|
|
65
|
+
path,
|
|
66
|
+
):
|
|
67
|
+
root = root[len(path) :]
|
|
68
|
+
if ".git" in dirs:
|
|
69
|
+
dirs.remove(".git")
|
|
70
|
+
paths.extend(
|
|
71
|
+
os.path.join(root, p).lstrip(os.path.sep)
|
|
72
|
+
for p in ps
|
|
73
|
+
if fnmatch(p, request.pattern)
|
|
74
|
+
and not gitignore(os.path.join(root, p).strip(os.path.sep))
|
|
75
|
+
)
|
|
76
|
+
if len(paths) > 0:
|
|
77
|
+
yield Message(message=f"[bold green]Success:[/] Found {len(paths)} files.")
|
|
78
|
+
else:
|
|
79
|
+
yield Message(
|
|
80
|
+
message=f"[red]Alert:[/] No files found matching the pattern {request.pattern}."
|
|
81
|
+
)
|
|
82
|
+
return ListFiles.Response(paths=paths)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Generator, List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from truefoundry.autodeploy.tools.base import (
|
|
9
|
+
Event,
|
|
10
|
+
Message,
|
|
11
|
+
RequestEvent,
|
|
12
|
+
ResponseEvent,
|
|
13
|
+
Tool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Line(BaseModel):
|
|
18
|
+
line_number: int
|
|
19
|
+
content: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReadFile(Tool):
|
|
23
|
+
description = """
|
|
24
|
+
Read contents of a file.
|
|
25
|
+
Avoid reading *.lock type files as they tend to be large
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
class Request(RequestEvent):
|
|
29
|
+
path: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
pattern=r"^[a-zA-Z0-9\.]{1}.*$",
|
|
32
|
+
description="File path to open. ",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class Response(ResponseEvent):
|
|
36
|
+
data: Optional[List[Line]] = Field(
|
|
37
|
+
None,
|
|
38
|
+
description="Content of the file.",
|
|
39
|
+
)
|
|
40
|
+
error: Optional[str] = Field(
|
|
41
|
+
None,
|
|
42
|
+
description="Error while opening a file.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
project_root_path: str,
|
|
48
|
+
):
|
|
49
|
+
self.project_root_path = project_root_path
|
|
50
|
+
|
|
51
|
+
def run(self, request: ReadFile.Request) -> Generator[Event, Any, ResponseEvent]:
|
|
52
|
+
yield Message(
|
|
53
|
+
message=f"[bold cyan]Processing:[/] Reading file at [magenta]{request.path}[/] and extracting details..."
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
with open(
|
|
57
|
+
os.path.join(self.project_root_path, request.path),
|
|
58
|
+
"r",
|
|
59
|
+
encoding="utf8",
|
|
60
|
+
) as f:
|
|
61
|
+
response = ReadFile.Response(data=[])
|
|
62
|
+
for i, line in enumerate(f):
|
|
63
|
+
response.data.append(Line(line_number=i + 1, content=line))
|
|
64
|
+
return response
|
|
65
|
+
except FileNotFoundError as ex:
|
|
66
|
+
return ReadFile.Response(error=str(ex))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Generator, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from truefoundry.autodeploy.tools.base import (
|
|
9
|
+
Event,
|
|
10
|
+
Message,
|
|
11
|
+
RequestEvent,
|
|
12
|
+
ResponseEvent,
|
|
13
|
+
Tool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SendRequest(Tool):
|
|
18
|
+
description = """
|
|
19
|
+
Send an HTTP request.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.call_count = 0
|
|
24
|
+
|
|
25
|
+
class Request(RequestEvent):
|
|
26
|
+
method: str
|
|
27
|
+
url: str
|
|
28
|
+
|
|
29
|
+
class Response(ResponseEvent):
|
|
30
|
+
response_code: Optional[int] = Field(None, description="Response Code")
|
|
31
|
+
response_body: Optional[str] = None
|
|
32
|
+
error: Optional[str] = Field(None, description="Error.")
|
|
33
|
+
|
|
34
|
+
def run(self, request: SendRequest.Request) -> Generator[Event, Any, ResponseEvent]:
|
|
35
|
+
self.call_count += 1
|
|
36
|
+
yield Message(
|
|
37
|
+
message=f"[bold cyan]Testing:[/] Sending a [magenta]{request.method.upper()}[/] request to [magenta]{request.url}[/]..."
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
response = requests.request(request.method.lower(), url=request.url)
|
|
41
|
+
yield Message(
|
|
42
|
+
message=f"[bold green]Success:[/] Received response with status code [magenta]{response.status_code}[/]"
|
|
43
|
+
)
|
|
44
|
+
return SendRequest.Response(
|
|
45
|
+
response_code=response.status_code,
|
|
46
|
+
response_body=response.text[-50:],
|
|
47
|
+
)
|
|
48
|
+
except Exception as ex:
|
|
49
|
+
yield Message(
|
|
50
|
+
message="[red]Alert:[/] Request could not be completed successfully."
|
|
51
|
+
)
|
|
52
|
+
return SendRequest.Response(
|
|
53
|
+
error=str(ex),
|
|
54
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# deprecated
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Confirm
|
|
10
|
+
|
|
11
|
+
from truefoundry.autodeploy.tools.base import Tool
|
|
12
|
+
from truefoundry.autodeploy.utils.diff import Diff
|
|
13
|
+
|
|
14
|
+
INTERACTIVE_SESSION = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WriteFile(Tool):
|
|
18
|
+
description = """
|
|
19
|
+
Write contents to a file.
|
|
20
|
+
Read first before write.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
class Request(BaseModel):
|
|
24
|
+
path: str = Field(
|
|
25
|
+
...,
|
|
26
|
+
pattern=r"^[a-zA-Z0-9\.]{1}.*$",
|
|
27
|
+
description="File path to write.",
|
|
28
|
+
)
|
|
29
|
+
content: str = Field(..., description="Content of the file.")
|
|
30
|
+
justification: str = Field(
|
|
31
|
+
...,
|
|
32
|
+
description="Justification of why the new content is required. Ensure the justification, justifies the content.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class Response(BaseModel):
|
|
36
|
+
cancellation_reason: Optional[str] = Field(
|
|
37
|
+
None, description="Operation cancelled by user"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
error: Optional[str] = Field(
|
|
41
|
+
None,
|
|
42
|
+
description="Error while opening a file.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_root_path: str):
|
|
46
|
+
self.project_root_path = project_root_path
|
|
47
|
+
|
|
48
|
+
def _user_interaction(
|
|
49
|
+
self, request: WriteFile.Request, console: Console
|
|
50
|
+
) -> Optional[WriteFile.Response]:
|
|
51
|
+
console.log("You are about to write or edit a file.")
|
|
52
|
+
|
|
53
|
+
console.print(f"Displaying changes in {request.path}")
|
|
54
|
+
prev_content = ""
|
|
55
|
+
|
|
56
|
+
if os.path.exists(os.path.join(self.project_root_path, request.path)):
|
|
57
|
+
with open(
|
|
58
|
+
os.path.join(self.project_root_path, request.path),
|
|
59
|
+
"r",
|
|
60
|
+
encoding="utf8",
|
|
61
|
+
) as f:
|
|
62
|
+
prev_content = f.read()
|
|
63
|
+
|
|
64
|
+
console.print(
|
|
65
|
+
Diff(
|
|
66
|
+
lhs=prev_content,
|
|
67
|
+
rhs=request.content,
|
|
68
|
+
width=os.get_terminal_size().columns,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
response = Confirm.ask(
|
|
72
|
+
f"Writing file at {request.path}?",
|
|
73
|
+
)
|
|
74
|
+
if not response:
|
|
75
|
+
description = console.input(
|
|
76
|
+
"You chose to cancel. Can you provide a reason why? [green]>> "
|
|
77
|
+
)
|
|
78
|
+
return WriteFile.Response(
|
|
79
|
+
cancellation_reason=description, error="Operation cancelled by user."
|
|
80
|
+
)
|
|
81
|
+
console.log(f"Writing file to {request.path}")
|
|
82
|
+
|
|
83
|
+
def run(
|
|
84
|
+
self,
|
|
85
|
+
request: WriteFile.Request,
|
|
86
|
+
console: Console,
|
|
87
|
+
) -> WriteFile.Response:
|
|
88
|
+
if INTERACTIVE_SESSION:
|
|
89
|
+
interaction_response = self._user_interaction(request, console)
|
|
90
|
+
if isinstance(interaction_response, WriteFile.Response):
|
|
91
|
+
return interaction_response
|
|
92
|
+
try:
|
|
93
|
+
with open(
|
|
94
|
+
os.path.join(self.project_root_path, request.path),
|
|
95
|
+
"w",
|
|
96
|
+
encoding="utf8",
|
|
97
|
+
) as f:
|
|
98
|
+
f.write(request.content)
|
|
99
|
+
return WriteFile.Response()
|
|
100
|
+
except FileNotFoundError as ex:
|
|
101
|
+
return WriteFile.Response(error=str(ex))
|