chore: import upstream snapshot with attribution
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:52 +08:00
commit e768098d0e
4004 changed files with 2804145 additions and 0 deletions
@@ -0,0 +1,62 @@
# Generate Python docstring
This example can help you automatically generate Python code's docstring and return the modified code.
Tools used in this flow
- `load_code` tool, it can load code from a file path.
- Load content from a local file.
- Loading content from a remote URL, currently loading HTML content, not just code.
- `divide_code` tool, it can divide code into code blocks.
- To avoid files that are too long and exceed the token limit, it is necessary to split the file.
- Avoid using the same function (such as __init__(self)) to generate docstrings in the same one file, which may cause confusion when adding docstrings to the corresponding functions in the future.
- `generate_docstring` tool, it can generate docstring for a code block, and merge docstring into origin code.
## What you will learn
In this flow, you will learn
- How to compose an auto generate docstring flow.
- How to use different LLM APIs to request LLM, including synchronous/asynchronous APIs, chat/completion APIs.
- How to use asynchronous multiple coroutine approach to request LLM API.
- How to construct a prompt.
## Prerequisites
### Install promptflow sdk and other dependencies:
```bash
pip install -r requirements.txt
```
### Create connection for LLM to use
```bash
# Override keys with --set to avoid yaml file changes
pf connection create --file ../../../connections/azure_openai.yml --set api_key=<your_api_key> api_base=<your_api_base>
```
Note:
The [azure_openai.yml](../../../connections/azure_openai.yml) file is located in connections folder.
We are using connection named `open_ai_connection`in [flow.dag.yaml](flow.dag.yaml).
## Execute with Promptflow
### Execute with SDK
`python main.py --source <your_file_path>`
**Note**: the file path should be a python file path, default is `./azure_open_ai.py`.
A webpage will be generated, displaying diff:
![result](result.png)
### Execute with CLI
```bash
# run flow with default file path in flow.dag.yaml
pf flow test --flow .
# run flow with file path
pf flow test --flow . --inputs source="./azure_open_ai.py"
```
```bash
# run flow with batch data
pf run create --flow . --data ./data.jsonl --name auto_generate_docstring --column-mapping source='${data.source}'
```
Output the code after add the docstring.
You can also skip providing `column-mapping` if provided data has same column name as the flow.
Reference [here](https://aka.ms/pf/column-mapping) for default behavior when `column-mapping` not provided in CLI.
@@ -0,0 +1,290 @@
import asyncio
import logging
import time
import uuid
from typing import List
from openai.version import VERSION as OPENAI_VERSION
import os
from abc import ABC, abstractmethod
import tiktoken
from dotenv import load_dotenv
from prompt import PromptLimitException
class AOAI(ABC):
def __init__(self, **kwargs):
if OPENAI_VERSION.startswith("0."):
raise Exception(
"Please upgrade your OpenAI package to version >= 1.0.0 or "
"using the command: pip install --upgrade openai."
)
init_params = {}
api_type = os.environ.get("API_TYPE")
if os.getenv("OPENAI_API_VERSION") is not None:
init_params["api_version"] = os.environ.get("OPENAI_API_VERSION")
if os.getenv("OPENAI_ORG_ID") is not None:
init_params["organization"] = os.environ.get("OPENAI_ORG_ID")
if os.getenv("OPENAI_API_KEY") is None:
raise ValueError("OPENAI_API_KEY is not set in environment variables")
if os.getenv("OPENAI_API_BASE") is not None:
if api_type == "azure":
init_params["azure_endpoint"] = os.environ.get("OPENAI_API_BASE")
else:
init_params["base_url"] = os.environ.get("OPENAI_API_BASE")
init_params["api_key"] = os.environ.get("OPENAI_API_KEY")
# A few sanity checks
if api_type == "azure":
if init_params.get("azure_endpoint") is None:
raise ValueError(
"OPENAI_API_BASE is not set in environment variables, this is required when api_type==azure"
)
if init_params.get("api_version") is None:
raise ValueError(
"OPENAI_API_VERSION is not set in environment variables, this is required when api_type==azure"
)
if init_params["api_key"].startswith("sk-"):
raise ValueError(
"OPENAI_API_KEY should not start with sk- when api_type==azure, "
"are you using openai key by mistake?"
)
from openai import AzureOpenAI as Client
from openai import AsyncAzureOpenAI as AsyncClient
else:
from openai import OpenAI as Client
from openai import AsyncClient as AsyncClient
self.client = Client(**init_params)
self.async_client = AsyncClient(**init_params)
self.default_engine = None
self.engine = kwargs.pop('model', None) or os.environ.get("MODEL")
self.total_tokens = 4000
self.max_tokens = kwargs.pop('max_tokens', None) or os.environ.get("MAX_TOKENS") or 1200
if self.engine == "gpt-4-32k":
self.total_tokens = 31000
if self.engine == "gpt-4":
self.total_tokens = 7000
if self.engine == "gpt-3.5-turbo-16k":
self.total_tokens = 15000
if self.max_tokens > self.total_tokens:
raise ValueError(f"max_tokens must be less than total_tokens, "
f"total_tokens is {self.total_tokens}, max_tokens is {self.max_tokens}")
self.tokens_limit = self.total_tokens - self.max_tokens
def count_tokens(self, text: str) -> int:
try:
encoding = tiktoken.encoding_for_model(self.engine)
except KeyError:
encoding = tiktoken.encoding_for_model(self.default_engine)
return len(encoding.encode(text))
def query(self, text, **kwargs):
stream = kwargs.pop("stream", False)
for i in range(3):
try:
if not stream:
return self.query_with_no_stream(text, **kwargs)
else:
return "".join(self.query_with_stream(text, **kwargs))
except Exception as e:
logging.error(f"Query failed, message={e}, "
f"will retry request llm after {(i + 1) * (i + 1)} seconds.")
time.sleep((i + 1) * (i + 1))
raise Exception("Query failed, and retry 3 times, but still failed.")
async def async_query(self, text, **kwargs):
stream = kwargs.pop("stream", False)
for i in range(3):
try:
if not stream:
res = await self.async_query_with_no_stream(text, **kwargs)
return res
else:
res = await self.async_query_with_stream(text, **kwargs)
return "".join(res)
except Exception as e:
logging.error(f"llm response error, message={e}, "
f"will retry request llm after {(i + 1) * (i + 1)} seconds.")
await asyncio.sleep((i + 1) * (i + 1))
raise Exception("llm response error, and retry 3 times, but still failed.")
@abstractmethod
def query_with_no_stream(self, text, **kwargs):
pass
@abstractmethod
def query_with_stream(self, text, **kwargs):
pass
@abstractmethod
async def async_query_with_no_stream(self, text, **kwargs):
pass
@abstractmethod
async def async_query_with_stream(self, text, **kwargs):
pass
class ChatLLM(AOAI):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.default_engine = "gpt-3.5-turbo"
self.engine = self.engine or self.default_engine
self.system_prompt = "You are a Python engineer."
self.conversation = dict()
def query_with_no_stream(self, text, **kwargs):
conversation_id = kwargs.pop('conversation', None)
messages = self.create_prompt(text, conversation_id)
self.validate_tokens(messages)
temperature = kwargs.pop("temperature", 0.1)
response = self.client.chat.completions.create(
model=self.engine,
messages=messages,
temperature=temperature,
max_tokens=self.max_tokens,
stream=False,
**kwargs,
)
response_role = response.choices[0].message.role
full_response = response.choices[0].message.content
self.add_to_conversation(text, "user", conversation_id=conversation_id)
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
return full_response
def query_with_stream(self, text, **kwargs):
conversation_id = kwargs.pop('conversation', None)
messages = self.create_prompt(text, conversation_id)
self.validate_tokens(messages)
temperature = kwargs.pop("temperature", 0.1)
response = self.client.chat.completions.create(
model=self.engine,
messages=messages,
temperature=temperature,
max_tokens=self.max_tokens,
stream=True,
**kwargs,
)
response_role = None
full_response = ""
for chunk in response:
delta = chunk.choices[0].delta
response_role = delta.role
if delta.content:
content = delta.content
full_response += content
yield content
self.add_to_conversation(text, "user", conversation_id=conversation_id)
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
async def async_query_with_no_stream(self, text, **kwargs):
conversation_id = kwargs.pop('conversation', None)
messages = self.create_prompt(text, conversation_id)
self.validate_tokens(messages)
temperature = kwargs.pop("temperature", 0.1)
response = await self.async_client.chat.completions.create(
model=self.engine,
messages=messages,
temperature=temperature,
max_tokens=self.max_tokens,
stream=False,
**kwargs,
)
response_role = response.choices[0].message.role
full_response = response.choices[0].message.content
self.add_to_conversation(text, "user", conversation_id=conversation_id)
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
return full_response
async def async_query_with_stream(self, text, **kwargs):
conversation_id = kwargs.pop('conversation', None)
messages = self.create_prompt(text, conversation_id)
self.validate_tokens(messages)
temperature = kwargs.pop("temperature", 0.1)
response = await self.async_client.chat.completions.create(
model=self.engine,
messages=messages,
temperature=temperature,
max_tokens=self.max_tokens,
stream=True,
**kwargs,
)
response_role = None
full_response = ""
for chunk in response:
delta = chunk.choices[0].delta
response_role = delta.role
if delta.content:
content = delta.content
full_response += content
yield content
self.add_to_conversation(text, "user", conversation_id=conversation_id)
self.add_to_conversation(full_response, response_role, conversation_id=conversation_id)
def get_unique_conversation_id(self):
return str(uuid.uuid4()).replace('-', '')
def add_to_conversation(self, message: str, role: str, conversation_id: str) -> None:
"""
Add a message to the conversation
"""
if type(conversation_id) is str:
self.conversation[conversation_id].append({"role": role, "content": message})
def del_conversation(self, conversation_id: str) -> None:
if conversation_id in self.conversation:
del self.conversation[conversation_id]
def init_conversation(self, conversation_id: str, system_prompt) -> None:
"""
Init a new conversation
"""
if type(conversation_id) is str:
self.conversation[conversation_id] = [{"role": "system", "content": system_prompt}]
def get_tokens_count(self, messages: List[dict]) -> int:
"""
Get token count
"""
num_tokens = 0
for message in messages:
# every message follows <im_start>{role/name}\n{content}<im_end>\n
num_tokens += 5
for key, value in message.items():
if value:
num_tokens += self.count_tokens(value)
if key == "name": # if there's a name, the role is omitted
num_tokens += 5 # role is always required and always 1 token
num_tokens += 5 # every reply is primed with <im_start>assistant
return num_tokens
def validate_tokens(self, messages: List[dict]) -> None:
total_tokens = self.get_tokens_count(messages)
if total_tokens > self.tokens_limit:
message = f"token count {total_tokens} exceeds limit {self.tokens_limit}"
raise PromptLimitException(message)
def create_prompt(self, text: str, conversation_id: str = None):
unique_conversation_id = self.get_unique_conversation_id()
conversation_id = conversation_id or unique_conversation_id
if conversation_id not in self.conversation:
self.init_conversation(conversation_id=conversation_id, system_prompt=self.system_prompt)
_conversation = self.conversation[conversation_id] + [{"role": "user", "content": text}]
while self.get_tokens_count(_conversation) > self.tokens_limit and len(_conversation) > 2:
_conversation.pop(1)
if unique_conversation_id == conversation_id:
self.del_conversation(conversation_id=unique_conversation_id)
return _conversation
if __name__ == "__main__":
load_dotenv()
llm = ChatLLM()
print(llm.query(text='how are you?'))
res = llm.query_with_stream(text='how are you?')
for item in res:
print(item)
@@ -0,0 +1 @@
{{divided|join('')}}
@@ -0,0 +1,9 @@
from promptflow.core import tool
from divider import Divider
from typing import List
@tool
def combine_code(divided: List[str]):
code = Divider.combine(divided)
return code
@@ -0,0 +1,3 @@
{"source": "./divider.py"}
{"source": "./azure_open_ai.py"}
{"source": "./generate_docstring_tool.py"}
@@ -0,0 +1,18 @@
import difflib
import webbrowser
def show_diff(left_content, right_content, name="file"):
d = difflib.HtmlDiff()
html = d.make_file(
left_content.splitlines(),
right_content.splitlines(),
"origin " + name,
"new " + name,
context=True,
numlines=20)
html = html.encode()
html_name = name + "_diff.html"
with open(html_name, "w+b") as fp:
fp.write(html)
webbrowser.open(html_name)
@@ -0,0 +1,9 @@
from promptflow.core import tool
from divider import Divider
@tool
def divide_code(file_content: str):
# Divide the code into several parts according to the global import/class/function.
divided = Divider.divide_file(file_content)
return divided
@@ -0,0 +1,112 @@
import logging
import re
from typing import List
class Settings:
divide_file = {
"py": r"(?<!.)(class|def)",
}
divide_func = {
"py": r"((\n {,6})|^)(class|def)\s+(\S+(?=\())\s*(\([^)]*\))?\s*(->[^:]*:|:) *"
}
class Divider:
language = 'py'
@classmethod
def divide_file(cls, text) -> List[str]:
matches = list(re.finditer(Settings.divide_file[Divider.language], text))
splitted_content = []
min_pos = matches[0].start() if len(matches) > 0 else len(text)
for i in range(len(matches)):
start = matches[i].start()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
splitted_content.append(text[start:end])
if min_pos != 0:
splitted_content.insert(0, text[0:min_pos])
return splitted_content
@classmethod
def divide_half(cls, text) -> List[str]:
"""
Divide the content into two parts, but ensure that the function body is not split.
"""
_, pos = Divider.get_functions_and_pos(text)
if len(pos) > 1: # Divide the code into two parts and every part start with a function.
i = len(pos) // 2
return [text[0:pos[i][0]], text[pos[i][0]:]]
if len(pos) == 1: # Divide the code into two parts, [function define + body, other body].
body = text[pos[0][1]:]
body_lines = body.split('\n')
body_ten_lines = '\n'.join(body_lines[0:10])
return [text[0:pos[0][1]] + body_ten_lines, body[len(body_ten_lines):]]
return [text]
@classmethod
def get_functions_and_pos(cls, text):
matches = re.finditer(Settings.divide_func[Divider.language], text)
functions = []
pos = []
for match in matches:
matched_text = match.group().replace('\n', '')
func = re.sub(r' +', ' ', matched_text).replace(' :', ':')
func = re.sub(r'[\s,]+\)', ')', func)
func = re.sub(r'\([\s,]+', '(', func)
functions.append(func.strip())
pos.append((match.start(), match.end()))
return functions, pos
@classmethod
def combine(cls, divided: List[str]):
return ''.join(divided)
@classmethod
def merge_doc2code(cls, docstring: str, origin_code: str) -> str:
funcs1, pos1 = Divider.get_functions_and_pos(docstring)
funcs2, pos2 = Divider.get_functions_and_pos(origin_code)
pattern = r'""".*?"""'
code = origin_code if len(funcs2) == 0 else origin_code[0:pos2[0][0]]
pos1.append((len(docstring), len(docstring))) # avoid index out of range
pos2.append((len(origin_code), len(origin_code))) # avoid index out of range
for i2 in range(len(funcs2)): # add docstring for each function in origin_code
part_full_code = origin_code[pos2[i2][0]:pos2[i2 + 1][0]]
try:
i1 = funcs1.index(funcs2[i2])
except ValueError:
logging.warning(f"No docstring found for {funcs2[i2]}")
code += part_full_code
continue
new_doc = re.findall(pattern, docstring[pos1[i1][1]:pos1[i1 + 1][0]], re.DOTALL)
if new_doc:
func_line = origin_code[pos2[i2][0]:pos2[i2][1]].replace('\n', '')
empty_line_num = (len(func_line) - len(func_line.lstrip()) + 4)
func_body = origin_code[pos2[i2][1]:pos2[i2 + 1][0]]
code_doc = list(re.finditer(pattern, func_body, re.DOTALL))
format_new_doc = Divider.format_indentation(new_doc[0], empty_line_num)
is_replace_doc = len(code_doc) > 0 and (re.sub(r'\s+', '', func_body[0:code_doc[0].start()]) == '')
if is_replace_doc:
code += part_full_code.replace(code_doc[0].group(), format_new_doc.strip(), 1)
else:
code += origin_code[pos2[i2][0]:pos2[i2][1]] + '\n' + format_new_doc + '\n' + origin_code[
pos2[i2][1]:
pos2[i2 + 1][0]]
else:
code += part_full_code
return code
@classmethod
def format_indentation(cls, text, empty_line_num):
lines = text.splitlines()
last_line_space_num = len(lines[-1]) - len(lines[-1].lstrip())
need_add_space = max(empty_line_num - last_line_space_num, 0) * ' '
lines[0] = last_line_space_num * ' ' + lines[0].lstrip() # Align the first row to the last row
indented_lines = [(need_add_space + line).rstrip() for line in lines]
indented_string = '\n'.join(indented_lines)
return indented_string
@classmethod
def has_class_or_func(cls, text):
funcs, _ = Divider.get_functions_and_pos(text)
return len(funcs) > 0
@@ -0,0 +1,48 @@
This is the docstring style of sphinx:
"""Description of the function.
:param [ParamName]: [ParamDescription](, defaults to [DefaultParamVal].)
:type [ParamName]: [ParamType](, optional)
...
:raises [ErrorType]: [ErrorDescription]
...
:return: [ReturnDescription]
:rtype: [ReturnType]
"""
Note:
For custom class types, please use the full path, for example:
"~azure.ai.ml.entities._inputs_outputs.Input" is full path for "Input" because of "from azure.ai.ml.entities._inputs_outputs import Input, Output"
"~import_node.Import" is full path for "Import" because of "import import_node.Import"
Complete function docstring example:
from azure.ai.ml.entities._inputs_outputs import Input, Output
from azure.ai.ml.constants import JobType
def output(input: Input, import_node: Import, startHnd=1, endHnd=None, uuids=None) -> Output:
"""Create an Output object.
:param input: The input object.
:type input: ~azure.ai.ml.entities._inputs_outputs.Input
:param import_node: The Import object.
:type import_node: ~import_node.Import
:param startHnd: Start index, defaults to 1
:type startHnd: int, optional
:param endHnd: End index, defaults to None
:type endHnd: int, optional
:return: The Output object.
:rtype: ~azure.ai.ml.entities._inputs_outputs.Output
"""
pass
Here's some code for you:
{{module}}
{{code}}
Please follow the sphinx style and refer above complete function docstring example, then output the docstring for the following class/functions.
Please replace "{docstring}" with the actual docstring.
{% for func in functions %}
{{func}}
{docstring}
pass
{% endfor %}
@@ -0,0 +1,83 @@
import logging
import os
from urllib.parse import urlparse
import requests
class File:
def __init__(self, source: str):
self._source = source
self._is_url = source.startswith("http://") or source.startswith("https://")
if self._is_url:
parsed_url = urlparse(source)
path = parsed_url.path
else:
path = source
self._path = os.path.normpath(os.path.abspath(path))
self._dirname = os.path.dirname(self._path)
self._filename = os.path.basename(self._path).split(".")[0]
self._language = os.path.basename(self._path).split(".")[1]
def _read_content(self):
if self._is_url:
response = requests.get(self.source)
if response.status_code == 200:
content = response.text
return content
else:
print(f"Failed to retrieve content from URL: {self.source}")
return None
else:
try:
with open(self._path, "r") as file:
content = file.read()
return content
except FileNotFoundError:
print(f"File not found: {self.source}")
return None
@property
def content(self) -> str:
if not hasattr(self, "_text"):
self._content = self._read_content()
return self._content
@property
def language(self) -> str:
return self._language
@property
def filename(self) -> str:
return self._filename
@property
def dirname(self) -> str:
return self._dirname
@property
def source(self) -> str:
return self._source
def override_origin_file(self, content: str) -> None:
if not self._is_url:
with open(self._path, "w") as f:
f.write(content)
else:
logging.warning(
"Cannot override origin file from URL, create a new file instead."
)
self.create_new_file(content)
def create_new_file(self, content: str) -> None:
if self._is_url:
path = os.path.join(
"./",
self.filename + f"_doc.{self.language}",
)
else:
path = os.path.join(
self.dirname,
self.filename + f"_doc.{self.language}",
)
with open(path, "w") as f:
f.write(content)
@@ -0,0 +1,42 @@
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
inputs:
source:
type: string
default: ./azure_open_ai.py
outputs:
code:
type: string
reference: ${combine_code.output}
nodes:
- name: load_code
type: python
source:
type: code
path: load_code_tool.py
inputs:
source: ${inputs.source}
- name: divide_code
type: python
source:
type: code
path: divide_code_tool.py
inputs:
file_content: ${load_code.output}
- name: generate_docstring
type: python
source:
type: code
path: generate_docstring_tool.py
inputs:
divided: ${divide_code.output}
connection: open_ai_connection
model: gpt-35-turbo
- name: combine_code
type: prompt
source:
type: code
path: combine_code.jinja2
inputs:
divided: ${generate_docstring.output}
environment:
python_requirements_txt: requirements.txt
@@ -0,0 +1,95 @@
import ast
import asyncio
import logging
import os
import sys
from typing import Union, List
from promptflow.core import tool
from azure_open_ai import ChatLLM
from divider import Divider
from prompt import docstring_prompt, PromptLimitException
from promptflow.connections import AzureOpenAIConnection, OpenAIConnection
def get_imports(content):
tree = ast.parse(content)
import_statements = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for n in node.names:
import_statements.append(f"import {n.name}")
elif isinstance(node, ast.ImportFrom):
module_name = node.module
for n in node.names:
import_statements.append(f"from {module_name} import {n.name}")
return import_statements
async def async_generate_docstring(divided: List[str]):
llm = ChatLLM()
divided = list(reversed(divided))
all_divided = []
# If too many imports result in tokens exceeding the limit, please set an empty string.
modules = '' # '\n'.join(get_imports(divided[-1]))
modules_tokens = llm.count_tokens(modules)
if modules_tokens > 300:
logging.warning(f'Too many imports, the number of tokens is {modules_tokens}')
if modules_tokens > 500:
logging.warning(f'Too many imports, the number of tokens is {modules_tokens}, will set an empty string.')
modules = ''
# Divide the code into two parts if the global class/function is too long.
while len(divided):
item = divided.pop()
try:
llm.validate_tokens(llm.create_prompt(docstring_prompt(code=item, module=modules)))
except PromptLimitException as e:
logging.warning(e.message + ', will divide the code into two parts.')
divided_tmp = Divider.divide_half(item)
if len(divided_tmp) > 1:
divided.extend(list(reversed(divided_tmp)))
continue
except Exception as e:
logging.warning(e)
all_divided.append(item)
tasks = []
last_code = ''
for item in all_divided:
if Divider.has_class_or_func(item):
tasks.append(llm.async_query(docstring_prompt(last_code=last_code, code=item, module=modules)))
else: # If the code has not function or class, no need to generate docstring.
tasks.append(asyncio.sleep(0))
last_code = item
res_doc = await asyncio.gather(*tasks)
new_code = []
for i in range(len(all_divided)):
if type(res_doc[i]) is str:
new_code.append(Divider.merge_doc2code(res_doc[i], all_divided[i]))
else:
new_code.append(all_divided[i])
return new_code
@tool
def generate_docstring(divided: List[str],
connection: Union[AzureOpenAIConnection, OpenAIConnection] = None,
model: str = None):
if isinstance(connection, AzureOpenAIConnection):
os.environ["OPENAI_API_KEY"] = connection.api_key
os.environ["OPENAI_API_BASE"] = connection.api_base
os.environ["OPENAI_API_VERSION"] = connection.api_version
os.environ["API_TYPE"] = connection.api_type
elif isinstance(connection, OpenAIConnection):
os.environ["OPENAI_API_KEY"] = connection.api_key
os.environ["ORGANIZATION"] = connection.organization
if model:
os.environ["MODEL"] = model
if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
return asyncio.run(async_generate_docstring(divided))
@@ -0,0 +1,8 @@
from promptflow.core import tool
from file import File
@tool
def load_code(source: str):
file = File(source)
return file.content
@@ -0,0 +1,18 @@
import argparse
from file import File
from diff import show_diff
from load_code_tool import load_code
from promptflow.client import PFClient
from pathlib import Path
if __name__ == "__main__":
current_folder = Path(__file__).absolute().parent
parser = argparse.ArgumentParser(description="The code path of code that need to generate docstring.")
parser.add_argument("--source", help="Path for the code file", default=str(current_folder / 'azure_open_ai.py'))
args = parser.parse_args()
pf = PFClient()
source = args.source
flow_result = pf.test(flow=str(current_folder), inputs={"source": source})
show_diff(load_code(source), flow_result['code'], File(source).filename)
@@ -0,0 +1,30 @@
import sys
from promptflow.tools.common import render_jinja_template
from divider import Divider
class PromptLimitException(Exception):
def __init__(self, message="", **kwargs):
super().__init__(message, **kwargs)
self._message = str(message)
self._kwargs = kwargs
self._inner_exception = kwargs.get("error")
self.exc_type, self.exc_value, self.exc_traceback = sys.exc_info()
self.exc_type = self.exc_type.__name__ if self.exc_type else type(self._inner_exception)
self.exc_msg = "{}, {}: {}".format(message, self.exc_type, self.exc_value)
@property
def message(self):
if self._message:
return self._message
return self.__class__.__name__
def docstring_prompt(last_code: str = '', code: str = '', module: str = '') -> str:
functions, _ = Divider.get_functions_and_pos(code)
# Add the first few lines to the function, such as decorator, to make the docstring generated better by llm.
first_three_lines = '\n'.join(last_code.split('\n')[-3:])
with open('doc_format.jinja2') as file:
return render_jinja_template(prompt=file.read(), module=module.strip('\n'),
code=(first_three_lines + code).strip('\n'), functions=functions)
@@ -0,0 +1,5 @@
promptflow[azure]
promptflow-tools
python-dotenv
jinja2
tiktoken
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB