nlsql-coder 0.0.1__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.
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+ from nlsql_coder.app.providers.config import ProviderConfig
3
+ from nlsql_coder.app.agent.model.agent import Agent
4
+ from nlsql_coder.app.agent.tools.tool_specs import read_file_spec, write_file_spec, get_project_tree_spec, edit_file_spec, insert_relative_spec, insert_to_file_spec
5
+ from nlsql_coder.app.cli.arg_parsing import args
6
+
7
+ provider_config = ProviderConfig()
8
+ agent = None
9
+ system_prompt = ""
10
+ with open(Path(__file__).resolve().parent / "system_prompt.md", "r") as f:
11
+ system_prompt = f.read()
12
+ agents_md = Path(args.workspace, "AGENTS.md")
13
+ if agents_md.exists():
14
+ with open(agents_md, "r") as f:
15
+ content = f.read()
16
+ system_prompt += content
17
+ tools = [read_file_spec, write_file_spec, get_project_tree_spec, edit_file_spec, insert_to_file_spec, insert_relative_spec]
18
+
19
+ if provider_config.model_provider == "aws":
20
+ from nlsql_coder.app.providers.aws.provider import AWSBedrockProvider
21
+ model_id = provider_config.model_name
22
+ llm = AWSBedrockProvider(model_id)
23
+ agent = Agent(llm, system_prompt, tools)
@@ -0,0 +1,6 @@
1
+ # from app.providers.base import Message
2
+
3
+ # class AgentMemory:
4
+
5
+
6
+ # def summarize_conversation():
@@ -0,0 +1,32 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ class Project:
6
+ """
7
+ Project class to gather context about the current project.
8
+ """
9
+ def __init__(self, root):
10
+ self.root = root
11
+ self.ignore_paths = [".venv", "venv", "__pycache__", ".git"]
12
+
13
+
14
+ def generate_tree(self, path: Path = None, indent: str = "", lines=None):
15
+ if lines is None:
16
+ lines = []
17
+ path = Path(self.root)
18
+ lines.append(f"{path.name}/")
19
+
20
+ try:
21
+ items = sorted([i for i in os.listdir(path) if i not in self.ignore_paths])
22
+ for item in items:
23
+ full_path = path / item
24
+ if full_path.is_dir():
25
+ lines.append(f"{indent} {item}/")
26
+ self.generate_tree(full_path, indent + " ", lines)
27
+ else:
28
+ lines.append(f"{indent} {item}")
29
+ except PermissionError:
30
+ lines.append(f"{indent} [Access Denied]")
31
+ return "\n".join(lines)
32
+
File without changes
@@ -0,0 +1,99 @@
1
+ """
2
+ Planner agent class.
3
+ Implements the functionallity for the planner agent.
4
+ """
5
+ from nlsql_coder.app.providers.base import BaseProvider, Message
6
+ from typing import Type, Iterator
7
+
8
+ from nlsql_coder.app.agent.tools.tool_specs import ToolSpecification
9
+
10
+ class Agent:
11
+ """
12
+ Main agent class, highest level class - first point of call for user messages.
13
+ Responsible for planning the steps involved in completing the give task and invoking other agents if needed.
14
+ """
15
+ def __init__(self, provider_class: Type[BaseProvider], system_prompt: str, tools: list[ToolSpecification] = None):
16
+ self.llm = provider_class
17
+ self.messages = []
18
+
19
+ self.kwargs = {
20
+ "system_prompt": system_prompt,
21
+ "tools": tools
22
+ }
23
+ if not tools:
24
+ self.kwargs.pop("tools")
25
+
26
+ def chat(self, message: str | list) -> Iterator[str | dict]:
27
+ if not message:
28
+ raise ValueError("Message is empty.")
29
+
30
+ # results = []
31
+ if isinstance(message, str):
32
+ user_message = Message(role="user", content=message)
33
+ self.messages.append(self.llm.form_message(user_message))
34
+ elif isinstance(message, list):
35
+ tool_result_messages = self.llm.form_message(message)
36
+ # content = tool_result_messages["content"]
37
+ # for item in content:
38
+ # results.append((item["toolResult"]["toolUseId"], item["toolResult"]["content"]))
39
+ self.messages.append(tool_result_messages)
40
+
41
+ response = self.llm.stream_chat(self.messages, **self.kwargs)
42
+
43
+ accumulated = ""
44
+ tools_used = []
45
+ usage_data = None
46
+ for chunk in response:
47
+ if isinstance(chunk, str):
48
+ if not "**Using tool:**" in chunk:
49
+ accumulated += chunk
50
+ yield chunk
51
+ elif isinstance(chunk, dict):
52
+ if chunk.get("input_tokens"):
53
+ usage_data = chunk
54
+ else:
55
+ for v in chunk.values():
56
+ tools_used.append(v)
57
+
58
+ if accumulated:
59
+ assistant_message = Message(role="assistant", content=accumulated.strip())
60
+ self.messages.append(self.llm.form_message(assistant_message))
61
+ if tools_used:
62
+ tool_message = {
63
+ "role": "assistant",
64
+ "content": [{"toolUse": tool} for tool in tools_used]
65
+ }
66
+ self.messages.append(tool_message)
67
+ yield tools_used
68
+ # else:
69
+ # assistant_message = Message(role="assistant", content=accumulated.strip())
70
+ # self.messages.append(self.llm.form_message(assistant_message))
71
+
72
+ # if results:
73
+ # kwargs = {
74
+ # "system_prompt": "You specialize in writing the shortest possible summaries of tool call results, respond ONLY with the summary and no other text."
75
+ # }
76
+
77
+ # for tool_id, result in results:
78
+ # msg = Message(role="user", content=f"Summarize the following tool result: {result}")
79
+ # msg = self.llm.form_message(msg)
80
+
81
+ # summary_stream = self.llm.stream_chat([msg], **kwargs)
82
+
83
+ # summary_text = ""
84
+ # for chunk in summary_stream:
85
+ # if isinstance(chunk, str):
86
+ # summary_text += chunk
87
+
88
+ # summary_text = summary_text.strip()
89
+
90
+ # # Replace the original tool result content with the summary
91
+ # for m in self.messages:
92
+ # if m["role"] == "user":
93
+ # for item in m.get("content", []):
94
+ # tool_result = item.get("toolResult")
95
+ # if tool_result and tool_result["toolUseId"] == tool_id:
96
+ # tool_result["content"] = [{"text": summary_text}]
97
+
98
+ if usage_data:
99
+ yield usage_data
File without changes
@@ -0,0 +1,147 @@
1
+ from nlsql_coder.app.agent.helpers.project_details import Project
2
+ from pathlib import Path
3
+ from nlsql_coder.app.cli.arg_parsing import args
4
+
5
+ FILE_TOOL_REGISTRY = {}
6
+
7
+ project = Project(args.workspace)
8
+
9
+ def get_project_tree():
10
+ """
11
+ Get's the current project's file tree
12
+ """
13
+ return project.generate_tree()
14
+
15
+
16
+ def read_file(file_path: str, from_line: int = 1, to_line: int = 100):
17
+ """
18
+ Reads a given file starting at from_line ending at to_line and returns its contents.
19
+ """
20
+ try:
21
+ root_path = Path(project.root).resolve()
22
+ target_path = (root_path / file_path).resolve()
23
+
24
+ if not str(target_path).startswith(str(root_path)):
25
+ return "error: access outside workspace is not allowed."
26
+ if from_line > to_line:
27
+ return "error: from_line must be less than or equal to to_line."
28
+
29
+ contents = []
30
+ with open(target_path, "r", encoding="utf-8") as f:
31
+ for i, line in enumerate(f, start=1):
32
+ if i > to_line:
33
+ break
34
+ if i >= from_line:
35
+ contents.append(line)
36
+ return "".join(contents)
37
+ except FileNotFoundError:
38
+ return "error: the file does not exist."
39
+ except IsADirectoryError:
40
+ return f"error: {file_path} is a directory."
41
+ except PermissionError:
42
+ return "error: you do not have permission to read this file."
43
+
44
+
45
+ def write_file(file_path: str, content: str):
46
+ """
47
+ Writes the given content to a new file at the specified file path.
48
+ """
49
+ try:
50
+ if not file_path:
51
+ return "error: file_path not given."
52
+ if not content:
53
+ return "error: no content given."
54
+ root_path = Path(project.root).resolve()
55
+ target_path = (root_path / file_path).resolve()
56
+ if not str(target_path).startswith(str(root_path)):
57
+ return "error: access outside workspace is not allowed."
58
+ with open(target_path, "w") as f:
59
+ f.write(content)
60
+ return "success"
61
+ except PermissionError:
62
+ return "error: you do not have permission to write to this file."
63
+ except Exception as e:
64
+ return f"error: there was an error writing the file: {e}"
65
+
66
+
67
+ def edit_file(file_path: str, edits: list[dict[str, str]]):
68
+ """Apply the given edits to a file."""
69
+ # TODO: This tool needs work, using replace will replace all instances (it's dangerous)
70
+ try:
71
+ root_path = Path(project.root).resolve()
72
+ target_path = (root_path / file_path).resolve()
73
+ contents = ""
74
+ with open(target_path, "r") as f:
75
+ contents = f.read()
76
+ for edit in edits:
77
+ if not edit["search"].endswith("\n"):
78
+ edit["search"] += "\n"
79
+ contents = contents.replace(edit["search"], edit["replace"])
80
+ with open(target_path, "w") as f:
81
+ f.write(contents)
82
+ return "success"
83
+ except FileNotFoundError:
84
+ return "error: the file does not exist."
85
+ except IsADirectoryError:
86
+ return f"error: {file_path} is a directory."
87
+ except PermissionError:
88
+ return "error: you do not have permission to edit this file."
89
+
90
+
91
+ def insert_to_file(file_path: str, code: str, mode: str = "append"):
92
+ """
93
+ mode:
94
+ - append
95
+ - prepend
96
+ """
97
+ root_path = Path(project.root).resolve()
98
+ target_path = (root_path / file_path).resolve()
99
+
100
+ with open(target_path, "r") as f:
101
+ contents = f.read()
102
+
103
+ if mode == "append":
104
+ contents = contents.rstrip() + "\n\n" + code + "\n"
105
+ elif mode == "prepend":
106
+ contents = code.rstrip() + "\n\n" + contents
107
+ else:
108
+ raise ValueError("Invalid mode")
109
+
110
+ with open(target_path, "w") as f:
111
+ f.write(contents)
112
+
113
+ return "success"
114
+
115
+
116
+ def insert_relative(file_path: str, anchor: str, code: str, position: str = "after"):
117
+ root_path = Path(project.root).resolve()
118
+ target_path = (root_path / file_path).resolve()
119
+
120
+ with open(target_path, "r") as f:
121
+ lines = f.readlines()
122
+
123
+ for i, line in enumerate(lines):
124
+ if anchor in line:
125
+ if position == "after":
126
+ lines.insert(i + 1, code + "\n")
127
+ elif position == "before":
128
+ lines.insert(i, code + "\n")
129
+ else:
130
+ raise ValueError("position must be before or after")
131
+ break
132
+ else:
133
+ raise ValueError("Anchor not found")
134
+
135
+ with open(target_path, "w") as f:
136
+ f.writelines(lines)
137
+
138
+ return "success"
139
+
140
+
141
+
142
+ FILE_TOOL_REGISTRY["read_file"] = read_file
143
+ FILE_TOOL_REGISTRY["write_file"] = write_file
144
+ FILE_TOOL_REGISTRY["get_project_tree"] = get_project_tree
145
+ FILE_TOOL_REGISTRY["edit_file"] = edit_file
146
+ FILE_TOOL_REGISTRY["insert_to_file"] = insert_to_file
147
+ FILE_TOOL_REGISTRY["insert_relative"] = insert_relative
@@ -0,0 +1,161 @@
1
+ """
2
+ Tool specifications & provider convertion functions
3
+ Current tools:
4
+ file_tools.py
5
+ read_file(file_path: str, from_line: int = 0, to_line: int = 100)
6
+ """
7
+ from nlsql_coder.app.providers.base import ToolSpecification, ToolSchema, ToolProperty
8
+
9
+
10
+ ############### Tools Specifications ###################
11
+
12
+ read_file_spec = ToolSpecification(
13
+ name="read_file",
14
+ description="Reads a given file starting at from_line ending at to_line and returns its contents.",
15
+ schema=ToolSchema(
16
+ type="object",
17
+ properties={
18
+ "file_path": ToolProperty(
19
+ type="string",
20
+ description="Path to the file to read (relative to the workspace root)."
21
+ ),
22
+ "from_line": ToolProperty(
23
+ type="integer",
24
+ description="Line number to start reading from (1-indexed). Defaults to 1.",
25
+ default=1
26
+ ),
27
+ "to_line": ToolProperty(
28
+ type="integer",
29
+ description="Line number to stop reading at (inclusive). Defaults to 100.",
30
+ default=100
31
+ )
32
+ },
33
+ required=["file_path"]
34
+ )
35
+ )
36
+
37
+ write_file_spec = ToolSpecification(
38
+ name="write_file",
39
+ description="Writes the contents to a new file at the given location",
40
+ schema=ToolSchema(
41
+ type="object",
42
+ properties={
43
+ "file_path": ToolProperty(
44
+ type="string",
45
+ description="Path to the file to write (relative to the workspace root (must include file name))."
46
+ ),
47
+ "content": ToolProperty(
48
+ type="string",
49
+ description="The content to write to the file.",
50
+ ),
51
+ },
52
+ required=["file_path", "content"]
53
+ )
54
+ )
55
+
56
+ edit_file_spec = ToolSpecification(
57
+ name="edit_file",
58
+ description="Apply one or more edits to a file.",
59
+ schema=ToolSchema(
60
+ properties={
61
+ "file_path": ToolProperty(
62
+ type="string",
63
+ description="File path"
64
+ ),
65
+ "edits": ToolProperty(
66
+ type="array",
67
+ description="List of edits",
68
+ items=ToolProperty(
69
+ type="object",
70
+ description="Single edit",
71
+ properties={
72
+ "search": ToolProperty(
73
+ type="string",
74
+ description="Text to find"
75
+ ),
76
+ "replace": ToolProperty(
77
+ type="string",
78
+ description="Replacement text"
79
+ ),
80
+ },
81
+ required=["search", "replace"]
82
+ )
83
+ )
84
+ },
85
+ required=["file_path", "edits"]
86
+ )
87
+ )
88
+
89
+ insert_to_file_spec = ToolSpecification(
90
+ name="insert_to_file",
91
+ description="Insert code into a file by appending or prepending it.",
92
+ schema=ToolSchema(
93
+ properties={
94
+ "file_path": ToolProperty(
95
+ type="string",
96
+ description="Path to the file"
97
+ ),
98
+ "code": ToolProperty(
99
+ type="string",
100
+ description="Code to insert into the file"
101
+ ),
102
+ "mode": ToolProperty(
103
+ type="string",
104
+ description="Where to insert the code: append or prepend",
105
+ )
106
+ },
107
+ required=["file_path", "code", "mode"]
108
+ )
109
+ )
110
+
111
+ insert_relative_spec = ToolSpecification(
112
+ name="insert_relative",
113
+ description="Insert code before or after a matching anchor line in a file.",
114
+ schema=ToolSchema(
115
+ properties={
116
+ "file_path": ToolProperty(
117
+ type="string",
118
+ description="Path to the file"
119
+ ),
120
+ "anchor": ToolProperty(
121
+ type="string",
122
+ description="Text to search for in a line (anchor point)"
123
+ ),
124
+ "code": ToolProperty(
125
+ type="string",
126
+ description="Code to insert"
127
+ ),
128
+ "position": ToolProperty(
129
+ type="string",
130
+ description="Where to insert relative to anchor: before or after",
131
+ )
132
+ },
133
+ required=["file_path", "anchor", "code", "position"]
134
+ )
135
+ )
136
+
137
+ get_project_tree_spec = ToolSpecification(
138
+ name="get_project_tree",
139
+ description="Gets the current project's file tree.",
140
+ schema=ToolSchema(
141
+ type="object",
142
+ properties={},
143
+ required=[]
144
+ )
145
+ )
146
+
147
+ #################################################
148
+
149
+
150
+ ########### Conversion functions ##################
151
+
152
+ def tool_aws_bedrock(tool: ToolSpecification):
153
+ return {
154
+ "toolSpec": {
155
+ "name": tool.name,
156
+ "description": tool.description,
157
+ "inputSchema": {
158
+ "json": tool.schema.serialize()
159
+ }
160
+ }
161
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ import argparse
2
+
3
+ arg_parser = argparse.ArgumentParser()
4
+ arg_parser.add_argument("-w", "--workspace", default=".", help="Path to the targe project's workspace.")
5
+ arg_parser.add_argument("-u", "--usage", action="store_true", help="Displayed usage data after each conversation turn.")
6
+
7
+ args = arg_parser.parse_args()
@@ -0,0 +1,96 @@
1
+ from nlsql_coder.app.agent.helpers.get_agent import agent
2
+ from nlsql_coder.app.cli.tool_calling import call_tools
3
+ from nlsql_coder.app.cli.arg_parsing import args
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.live import Live
8
+ from rich.align import Align
9
+ from nlsql_coder.data.create_agent_md import create_md
10
+ import time
11
+
12
+
13
+ console = Console()
14
+
15
+ welcome_message = """[blue bold]\n
16
+ Welcome _ _ _ ____ ___ _
17
+ | \ | || | / ___| / _ \| |
18
+ to | \| || | \___ \| | | | |
19
+ | |\ || |___ ___) | |_| | |___
20
+ |_| \_||_____||____/ \__\_\_____| Code\n\n[/blue bold]
21
+ """
22
+
23
+
24
+
25
+ def main_loop():
26
+ try:
27
+ usage_data = {}
28
+ console.clear()
29
+ with console.status("[bold]Creating AGENTS.md file...\n[/bold]", spinner="dots"):
30
+ success = create_md()
31
+ if success == "created":
32
+ console.print("[bold]AGENTS.md file created at project root.\n[/bold]")
33
+ elif success == "exists":
34
+ console.print("[bold]AGENTS.md file already exists at project root.\n[/bold]")
35
+ elif success == "empty":
36
+ console.print("[bold]Starting from scratch!\n[/bold]")
37
+ else:
38
+ console.print("[bold]Failed to create AGENTS.md file.\n[/bold]")
39
+ time.sleep(2)
40
+ console.clear()
41
+ console.print(Align.center(welcome_message))
42
+
43
+ while True:
44
+
45
+ if args.usage and usage_data:
46
+ table = (
47
+ "| Input Tokens | Output Tokens | Total Tokens |\n"
48
+ "|--------------|---------------|---------------|\n"
49
+ f"| {usage_data.get('input_tokens')} | {usage_data.get('output_tokens')} | {usage_data.get('total_tokens')} |\n"
50
+ )
51
+ console.print("[bold]\n\nUsage Data[/bold]")
52
+ console.print(Markdown(table))
53
+
54
+ messages = console.input("\n[bold green][You]:[/bold green] ")
55
+
56
+ if messages.lower() in ["quit", "exit"]:
57
+ console.print("\n\n[bold purple]Goodbye[/bold purple]\n")
58
+ break
59
+
60
+ console.print("\n[bold cyan][Assistant]:[/bold cyan]")
61
+
62
+ while True:
63
+ usage_data = {}
64
+
65
+ found_tool_calls = False
66
+ buffer = ""
67
+ with Live(console=console, refresh_per_second=1) as live:
68
+ for chunk in agent.chat(messages):
69
+ if isinstance(chunk, str):
70
+ # Chat message chunk
71
+ buffer += chunk
72
+ live.update(Markdown(buffer))
73
+ elif isinstance(chunk, list):
74
+ # Tool execution
75
+ found_tool_calls = True
76
+ tool_results = call_tools(chunk)
77
+ messages = tool_results
78
+ elif isinstance(chunk, dict):
79
+ # Usage data
80
+ if args.usage:
81
+ if not usage_data:
82
+ usage_data["input_tokens"] = chunk["input_tokens"]
83
+ usage_data["output_tokens"] = chunk["output_tokens"]
84
+ usage_data["total_tokens"] = chunk["total_tokens"]
85
+ else:
86
+ usage_data["input_tokens"] += chunk["input_tokens"]
87
+ usage_data["output_tokens"] += chunk["output_tokens"]
88
+ usage_data["total_tokens"] += chunk["total_tokens"]
89
+
90
+
91
+ if not found_tool_calls:
92
+ break
93
+ except KeyboardInterrupt:
94
+ console.print("\n\n[bold purple]Goodbye[/bold purple]\n")
95
+ except Exception as e:
96
+ console.print(f"\n\n[bold red]Oh no! Something went wrong. Here's the error:\n\n{e}[/bold red]\n")
@@ -0,0 +1,27 @@
1
+ from nlsql_coder.app.agent.tools.file_tools import FILE_TOOL_REGISTRY
2
+
3
+ def call_tools(tool_calls: list[dict]) -> list[dict]:
4
+ tool_results = []
5
+ for tool in tool_calls:
6
+ tool_id = tool["toolUseId"]
7
+ tool_name = tool["name"]
8
+ tool_input = tool["input"]
9
+
10
+ tool_function = FILE_TOOL_REGISTRY.get(tool_name)
11
+
12
+ if tool_function:
13
+ tool_results.append({
14
+ "toolUseId": tool_id,
15
+ "name": tool_name,
16
+ "result": tool_function(**tool_input),
17
+ "status": "success"
18
+ })
19
+ else:
20
+ tool_results.append({
21
+ "toolUseId": tool_id,
22
+ "name": tool_name,
23
+ "result": "error: Unknown tool.",
24
+ "status": "error"
25
+ })
26
+
27
+ return tool_results
File without changes
File without changes
@@ -0,0 +1,139 @@
1
+ from nlsql_coder.app.providers.base import BaseProvider, Message
2
+ from typing import Iterator
3
+ from nlsql_coder.app.providers.config import AWSConfig
4
+ from nlsql_coder.app.agent.tools.tool_specs import ToolSpecification, tool_aws_bedrock
5
+ import boto3
6
+ import json
7
+
8
+
9
+ class AWSBedrockProvider(BaseProvider):
10
+
11
+ def __init__(self, model_id: str):
12
+ config = AWSConfig()
13
+
14
+ self.model_id = model_id
15
+ self.client = boto3.client(
16
+ "bedrock-runtime",
17
+ region_name=config.region,
18
+ aws_access_key_id=config.access_key_id,
19
+ aws_secret_access_key=config.secret_access_key,
20
+ )
21
+
22
+ def form_message(self, message: Message | list[dict]) -> dict:
23
+ if isinstance(message, Message):
24
+ if isinstance(message.content, str): # Standard chat message
25
+ return {"role": message.role, "content": [{"text": message.content}]}
26
+ if isinstance(message.content, dict):
27
+ if message.content["type"] == "tool_use":
28
+ return {"role": message.role, "content": [{"toolUse": message.content}]} # Tool call message
29
+ else:
30
+ results = {
31
+ "role": "user",
32
+ "content": [
33
+ {
34
+ "toolResult": {
35
+ "toolUseId": m["toolUseId"],
36
+ "content": [{"text": m["result"]}],
37
+ "status": m["status"]
38
+ }
39
+ }
40
+ for m in message
41
+ ]
42
+ }
43
+ return results
44
+
45
+
46
+ def stream_chat(self, messages: list[Message],
47
+ max_tokens: int = 8192,
48
+ temperature: float = 0.7,
49
+ system_prompt: str = "",
50
+ tools: list[ToolSpecification] | None = None) -> Iterator[str]:
51
+
52
+ system_message = [{"text": system_prompt}]
53
+ inference_config = {"maxTokens": max_tokens}
54
+
55
+ if not "opus-4-7" in self.model_id:
56
+ inference_config["temperature"] = temperature
57
+
58
+ kwargs = {
59
+ "modelId": self.model_id,
60
+ "messages": messages,
61
+ "system": system_message,
62
+ "inferenceConfig": inference_config,
63
+ }
64
+
65
+ if tools:
66
+ tools = [tool_aws_bedrock(tool) for tool in tools]
67
+ kwargs["toolConfig"] = {"tools": tools}
68
+
69
+ response = self.client.converse_stream(**kwargs)
70
+
71
+ tool_calls = {}
72
+ usage_data = {}
73
+
74
+ for event in response["stream"]:
75
+ if "contentBlockStart" in event:
76
+ idx = event["contentBlockStart"]["contentBlockIndex"]
77
+ start_event = event["contentBlockStart"]["start"]
78
+ if "toolUse" in start_event:
79
+ tool_use = start_event['toolUse']
80
+ tool_calls[idx] = {
81
+ "toolUseId": tool_use["toolUseId"] ,
82
+ "name": tool_use["name"],
83
+ "input": "",
84
+ "type": tool_use["type"]
85
+ }
86
+ yield f"\n\n**Using tool:** {tool_use['name']}\n" # User-facing tool call message
87
+
88
+ if "contentBlockDelta" in event:
89
+ delta_event = event["contentBlockDelta"]
90
+ idx = delta_event["contentBlockIndex"]
91
+ if "delta" in delta_event:
92
+ delta = delta_event["delta"]
93
+ if "text" in delta:
94
+ yield delta["text"] # Message chunk output
95
+ if "toolUse" in delta:
96
+ tool_input_chunk = delta["toolUse"]["input"] # Tool input chunks
97
+ if idx in tool_calls:
98
+ tool_calls[idx]["input"] += tool_input_chunk
99
+
100
+ if "contentBlockStop" in event:
101
+ idx = event["contentBlockStop"]["contentBlockIndex"]
102
+ if tool_calls:
103
+ # Turn tool input json string to dict
104
+ for v in tool_calls.values():
105
+ if isinstance(v.get("input"), str):
106
+ raw_val = v["input"].strip()
107
+ if raw_val:
108
+ try:
109
+ v["input"] = json.loads(raw_val)
110
+ if v.get("name") == "read_file":
111
+ yield f"**Reading {v['input'].get('file_path')}**\n\n"
112
+ if v.get("name") in ["edit_file", "insert_to_file", "insert_relative"]:
113
+ yield f"**Editing {v['input'].get('file_path')}**\n\n"
114
+ if v.get("name") == "write_file":
115
+ yield f"**Writing {v['input'].get('file_path')}**\n\n"
116
+ except json.JSONDecodeError:
117
+ v["input"] = {}
118
+ else:
119
+ v["input"] = {}
120
+
121
+ if "metadata" in event:
122
+ metadata = event["metadata"]
123
+ if "usage" in metadata:
124
+ usage = metadata["usage"]
125
+ usage_data = {
126
+ "input_tokens": usage["inputTokens"],
127
+ "output_tokens": usage["outputTokens"],
128
+ "total_tokens": usage["totalTokens"]
129
+ }
130
+
131
+ # If tools were called yield the calls and execute them in the agent class
132
+ if tool_calls:
133
+ yield tool_calls
134
+ if usage_data:
135
+ yield usage_data
136
+
137
+
138
+
139
+
@@ -0,0 +1,81 @@
1
+ """
2
+ Base provider class.
3
+ Normalizes LLM interaction and responses amoung the different LLM providers.
4
+ """
5
+ from abc import ABC, abstractmethod
6
+ from typing import Iterator, Literal, Dict, Optional, Any, List
7
+ from dataclasses import dataclass, field
8
+
9
+ @dataclass
10
+ class Message:
11
+ role: Literal["user", "assistant", "system"]
12
+ content: str | dict
13
+
14
+
15
+ @dataclass
16
+ class ToolProperty:
17
+ type: str
18
+ description: str
19
+ default: Optional[Any] = None
20
+ items: Optional["ToolProperty"] = None
21
+ properties: Optional[Dict[str, "ToolProperty"]] = None
22
+ required: Optional[List[str]] = None
23
+
24
+ @dataclass
25
+ class ToolSchema:
26
+ type: str = "object"
27
+ properties: Dict[str, ToolProperty] = field(default_factory=dict)
28
+ required: List[str] = field(default_factory=list)
29
+
30
+ def serialize(self):
31
+ return serialize_schema(self)
32
+
33
+ @dataclass
34
+ class ToolSpecification:
35
+ name: str
36
+ description: str
37
+ schema: ToolSchema
38
+
39
+
40
+ def serialize_property(prop: ToolProperty):
41
+ result = {
42
+ "type": prop.type,
43
+ "description": prop.description
44
+ }
45
+
46
+ if prop.default is not None:
47
+ result["default"] = prop.default
48
+
49
+ if prop.type == "array" and prop.items:
50
+ result["items"] = serialize_property(prop.items)
51
+
52
+ if prop.type == "object" and prop.properties:
53
+ result["properties"] = {
54
+ k: serialize_property(v) for k, v in prop.properties.items()
55
+ }
56
+ result["required"] = prop.required or []
57
+ result["additionalProperties"] = False
58
+
59
+ return result
60
+
61
+ def serialize_schema(schema: ToolSchema):
62
+ result = {
63
+ "type": schema.type,
64
+ "properties": {
65
+ k: serialize_property(v)
66
+ for k, v in schema.properties.items()
67
+ },
68
+ "required": schema.required or [],
69
+ "additionalProperties": False
70
+ }
71
+ return result
72
+
73
+
74
+ class BaseProvider(ABC):
75
+ @abstractmethod
76
+ def stream_chat(self, messages: list[Message], **kwargs) -> Iterator[str]:
77
+ pass
78
+
79
+ @abstractmethod
80
+ def form_message(self, message: Message):
81
+ pass
@@ -0,0 +1,22 @@
1
+ """
2
+ Provider configuration.
3
+ """
4
+ from dataclasses import dataclass
5
+ from dotenv import load_dotenv
6
+ import os
7
+
8
+ load_dotenv()
9
+
10
+ # Provider & Model config
11
+ @dataclass
12
+ class ProviderConfig:
13
+ model_provider: str = os.getenv("MODEL_PROVIDER", "")
14
+ model_name: str = os.getenv("MODEL_NAME", "")
15
+
16
+
17
+ # AWS Config
18
+ @dataclass
19
+ class AWSConfig:
20
+ access_key_id: str = os.getenv("AWS_ACCESS_KEY_ID", "")
21
+ secret_access_key: str = os.getenv("AWS_SECRET_ACCESS_KEY", "")
22
+ region: str = os.getenv("AWS_DEFAULT_REGION", "")
File without changes
File without changes
@@ -0,0 +1,76 @@
1
+ from nlsql_coder.app.agent.helpers.project_details import Project
2
+ from nlsql_coder.app.cli.arg_parsing import args
3
+ from nlsql_coder.app.agent.helpers.get_agent import llm
4
+ from nlsql_coder.app.agent.model.agent import Agent
5
+ from pathlib import Path
6
+
7
+ # Get relevant project files for context
8
+ def create_md():
9
+ try:
10
+ root = Path(args.workspace)
11
+ if Path(root, "AGENTS.md").exists():
12
+ return "exists"
13
+ patterns = ["README*", "readme*", "requirements*", "package.json", "*.md", "*.txt", "main.*", "run.*", "Dockerfile", "*compose*.yml", "*compose*.yaml", "index.html"]
14
+ exclude_dirs = {".venv", "node_modules", ".git", "__pycache__"}
15
+
16
+ files = []
17
+ for pattern in patterns:
18
+ for f in root.rglob(pattern):
19
+ if not any(part in exclude_dirs for part in f.parts):
20
+ files.append(f)
21
+ if not files:
22
+ return "empty"
23
+
24
+ project = Project(args.workspace)
25
+
26
+ project_tree = project.generate_tree()
27
+
28
+ overview_creator_prompt = """
29
+ # Role
30
+ You are a coding project summerizer
31
+
32
+ # Task
33
+ Summarize the project based on the given context and extract any important information for the development agent's context.
34
+
35
+ Your summary should include:
36
+ - Brief overview of the project (i.e. name, what it does, who it's for.)
37
+ - Commands/steps to buidl and run the application (if applicable)
38
+ - Project archetecture (i.e. main components, frontend, backend, languages used)
39
+ - Any other relevant observations or notes
40
+
41
+ # Response rules
42
+ - Respond ONLY with the project summary
43
+ - Respond in Markdown format
44
+ - Keep the summary consice and on-topic
45
+ """
46
+ overview_creator = Agent(llm, overview_creator_prompt)
47
+
48
+ file_contents = ""
49
+ for file in files:
50
+ with open(Path(root, file), "r") as f:
51
+ file_contents += f"{str(file)}:\n{f.read()}"
52
+
53
+ context_message = f"""
54
+ CONTEXT:
55
+ {file_contents}
56
+ """
57
+ accumulated = ""
58
+ response = overview_creator.chat(context_message)
59
+ for chunk in response:
60
+ if isinstance(chunk, str):
61
+ accumulated += chunk
62
+
63
+ md = f"""
64
+ # Project Tree
65
+ ```
66
+ {project_tree}
67
+ ```
68
+ {accumulated}
69
+ """
70
+
71
+ with open(Path(root, "AGENTS.md"), "w") as f:
72
+ f.write(md)
73
+
74
+ return "created"
75
+ except Exception as e:
76
+ return False
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: nlsql-coder
3
+ Version: 0.0.1
4
+ Summary: NLSQL's internal AI coding agent CLI tool for interacting with codebases
5
+ Author: NLSQL
6
+ License: MIT
7
+ Keywords: ai,agent,cli,coding,llm
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Environment :: Console
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: boto3<2.0,>=1.34
16
+ Requires-Dist: python-dotenv>=1.0
17
+ Requires-Dist: rich>=13.0.0
18
+ Requires-Dist: faiss-cpu>=1.7.0
19
+ Dynamic: license-file
20
+
21
+ # NLSQL Internal Coding Agent
22
+
23
+ NLSQL's internal coding agent is a private, tool-driven, workspace-aware AI system designed to help with both internal and client-based development tasks in the NLSQL ecosystem.
24
+
25
+ # Features
26
+
27
+ - User-friendly command line interface.
28
+ - Wide array of LLM/provider support acorss many cloud ecosystems.
29
+ - Primary privacy & security focus.
30
+ - Parallel tool calling ability for speed and efficency.
31
+
32
+ # How it Works
33
+
34
+ The agent operates using a ReAct-style loop. A user submits a prompt, and the agent processes it by reasoning about the task, performing actions using available tools, and evaluating the results.
35
+
36
+ If the outcome is insufficient or incomplete, the agent continues this cycle, refining its approach and taking further actions until it can produce a final response.
37
+
38
+ # Usage
39
+
40
+ To use the application, follow the steps below:
41
+
42
+ 1. Clone this repo to your local system: `git clone <repo-link-here>`
43
+ 2. Make sure you are at `cd internal-coding-agent`
44
+ 3. Create a virtual environment: `python3 -m venv .venv`
45
+ 4. Activate the venv: `source .venv/bin/activate`
46
+ 6. Install requirements: (if venv is activated correctly you should see "(venv)" in your terminal) `pip install -r requirements.txt`
47
+ 7. Add .env file into `cd internal-coding-agent` root folder
48
+ 8. Run the application: `python3 run.py -u --workspace path/to/your/project`
49
+
50
+ ## Arguments
51
+
52
+ | Short | Long | Description | Defaults to | Required |
53
+ |-|-|-|-|-|
54
+ | `-w` | `--workspace` | The root directory of the project you want to work on. | `.` | ✔ |
55
+ | `-u` | `--usage` | Displays usage data after each conversation turn (input & output tokens) | `False` | ✗ |
@@ -0,0 +1,30 @@
1
+ nlsql_coder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nlsql_coder/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ nlsql_coder/app/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ nlsql_coder/app/agent/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ nlsql_coder/app/agent/helpers/get_agent.py,sha256=z10raBVCj_lyvqJdxQoR1GzCXn28eLzKYvqylGNJf3k,1052
6
+ nlsql_coder/app/agent/helpers/memory_manager.py,sha256=0glsSiXsmQeICKCMADUDG4oPjd5WuB5n2J0JKZEBOPE,96
7
+ nlsql_coder/app/agent/helpers/project_details.py,sha256=82lCMZSINTkGKcWn2XVeIDK61m7ulba5ZkdCzr1xABE,1004
8
+ nlsql_coder/app/agent/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ nlsql_coder/app/agent/model/agent.py,sha256=0FLqfSC_WIN798X31tyVryTYRdS_sAWGEzVFdeD8O1s,3989
10
+ nlsql_coder/app/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ nlsql_coder/app/agent/tools/file_tools.py,sha256=1MFW1rjJaaKBReyvCQUFzsf3hMia6uFx8krL-enY2is,4832
12
+ nlsql_coder/app/agent/tools/tool_specs.py,sha256=Fqj-mn8D-gsopuXONO3Q8FsHqSnKX8lyMSMlIYZrkVQ,4977
13
+ nlsql_coder/app/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ nlsql_coder/app/cli/arg_parsing.py,sha256=NLI7zRjLeMi5gQ7nCwj98xYRbksnKk9pf2U3ItCAM6U,313
15
+ nlsql_coder/app/cli/interface.py,sha256=L9DW60tev4I2Ubz7op77-vigqMuEwUgK26QbCSAj0dU,3968
16
+ nlsql_coder/app/cli/tool_calling.py,sha256=DhnKnTiGHesh1Wr8nVHPduCpjeSTSM6ZyATbNbepoTk,835
17
+ nlsql_coder/app/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ nlsql_coder/app/providers/base.py,sha256=bYcoU5KLfZ0GYECwlmQfTFMXv6G8MinP1wV_71R82Wc,2056
19
+ nlsql_coder/app/providers/config.py,sha256=y8VavW7eVhmaGxg2dVFBYBi_HR7dEOm9BapDaJBU8nE,515
20
+ nlsql_coder/app/providers/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ nlsql_coder/app/providers/aws/provider.py,sha256=M0JigWjybTcVOjth44d_V2hk74bCm2xaafvOn4-gsBc,5712
22
+ nlsql_coder/app/providers/gcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ nlsql_coder/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ nlsql_coder/data/create_agent_md.py,sha256=f-bP5DGAILP7jlf5dmvZHhjewZFl3TZxKlwHoNVxBL8,2550
25
+ nlsql_coder-0.0.1.dist-info/licenses/LICENSE,sha256=GNO7NFi7navN9edNrPJWozm4pkhjOKsJqXuoZdMhSSg,1072
26
+ nlsql_coder-0.0.1.dist-info/METADATA,sha256=0y-QUYp_VtYB1Qb6QECChZOUISHLJUSrNuUPeoa9GT4,2263
27
+ nlsql_coder-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ nlsql_coder-0.0.1.dist-info/entry_points.txt,sha256=BK04QmRGok7YDWL-dBySr-gghDPK-iQcJ3S7PoifNwM,72
29
+ nlsql_coder-0.0.1.dist-info/top_level.txt,sha256=J16-Bz42sGpUXjB1uWqE66v4JPIiymCnGc7LhJkt_60,12
30
+ nlsql_coder-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nlsql-coder = nlsql_coder.app.cli.interface:main_loop
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1 @@
1
+ nlsql_coder