Akjava commited on
Commit
af9aea6
·
1 Parent(s): 54a35e3
Files changed (5) hide show
  1. .gitignore +3 -0
  2. app.py +254 -0
  3. linear_api_utils.py +100 -0
  4. requirements.txt +4 -0
  5. sleep_per_last_token_model.py +56 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__
2
+ .gradio
3
+ .env
app.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import gradio as gr
4
+ from smolagents import CodeAgent, tool
5
+
6
+ from linear_api_utils import execute_query
7
+ from sleep_per_last_token_model import SleepPerLastTokenModelLiteLLM
8
+
9
+ # .env
10
+ """
11
+ LINEAR_API_KEY="lin_api_***"
12
+ HF_TOKEN = "hf_***"
13
+ GROQ_API_KEY = "gsk_***"
14
+ """
15
+
16
+
17
+ def get_env_value(key, is_value_error_on_null=True):
18
+ value = os.getenv(key)
19
+ if value is None:
20
+ from dotenv import load_dotenv
21
+
22
+ load_dotenv()
23
+ value = os.getenv(key)
24
+ if is_value_error_on_null and value is None:
25
+ raise ValueError(f"Need {key} on secret or .env(If running on local)")
26
+ return value
27
+
28
+
29
+ # SETTINGS
30
+ LINEAR_ISSUE_LABEL = "huggingface-public" # only show issue with this label,I added for demo you can remove this
31
+ ## set secret key on Space setting or .env(local)
32
+ # hf_token = get_env_value("HF_TOKEN")
33
+ groq_api_key = get_env_value("GROQ_API_KEY")
34
+ api_key = get_env_value("LINEAR_API_KEY")
35
+
36
+
37
+ if api_key is None:
38
+ raise ValueError("Need LINEAR_API_KEY on secret")
39
+ if groq_api_key is None:
40
+ raise ValueError("Need GROQ_API_KEY on secret")
41
+
42
+
43
+ model_id = "groq/llama3-8b-8192"
44
+
45
+
46
+ def add_comment(issue_id, model_name, comment):
47
+ comment = comment.replace('"', '\\"').replace("\n", "\\n") # escape doublequote
48
+ # header = f"<!---\\n start-ai-comment({model_name}) \\n--->\\n"
49
+ header = f"[ ](start-ai-comment:{model_name})\\n"
50
+ header += f"# {model_name.split('/')[1]}'s comment'\\n"
51
+ comment = header + comment
52
+ comment_create_text = """
53
+ mutation CommentCreate {
54
+ commentCreate(
55
+ input: {
56
+ issueId : "%s"
57
+ body:"%s"
58
+ }
59
+ ) {
60
+ success
61
+ comment {
62
+ id
63
+ body
64
+ }
65
+ }
66
+ }""" % (issue_id, comment)
67
+ result = execute_query("add comment", comment_create_text, api_key)
68
+
69
+
70
+ issue_id = None
71
+
72
+
73
+ def change_state_reviewing():
74
+ get_state_query_text = """
75
+ query Sate{
76
+ workflowStates(filter:{team:{id:{eq:"%s"}}}){
77
+ nodes{
78
+ id
79
+ name
80
+ }
81
+ }
82
+ }
83
+ """ % (team_id)
84
+ result = execute_query("State", get_state_query_text, api_key)
85
+ state_id = None
86
+ for state in result["data"]["workflowStates"]["nodes"]:
87
+ if state["name"] == "Reviewing":
88
+ state_id = state["id"]
89
+ break
90
+
91
+ if state_id is None:
92
+ return
93
+ issue_update_text = """
94
+ mutation IssueUpdate {
95
+ issueUpdate(
96
+ id: "%s",
97
+ input: {
98
+ stateId: "%s",
99
+ }
100
+ ) {
101
+ success
102
+ issue {
103
+ id
104
+ title
105
+ state {
106
+ id
107
+ name
108
+ }
109
+ }
110
+ }
111
+ }
112
+ """ % (issue_id, state_id)
113
+ result = execute_query("IssueUpdate", issue_update_text, api_key)
114
+
115
+
116
+ @tool
117
+ def get_todo_issue() -> str:
118
+ """
119
+ Get the Todo issue.
120
+
121
+ Returns:
122
+ A string describing the current issue.
123
+ """
124
+ global issue_id
125
+ global issue_text
126
+ priority_order = [1, 2, 3, 0, 4]
127
+ for priority in priority_order:
128
+ team_query_text = """
129
+ query Team {
130
+ team(id: "%s") {
131
+ id
132
+ issues(first:1,filter:{
133
+ state:{
134
+ name:{ eq: "Todo" },
135
+ }
136
+ priority:{eq:%d}
137
+ }) {
138
+ nodes {
139
+ id
140
+ title
141
+ description
142
+ createdAt
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ """ % (team_id, priority)
149
+
150
+ result = execute_query("Team", team_query_text, api_key, True)
151
+ if len(result["data"]["team"]["issues"]["nodes"]) > 0:
152
+ issue = result["data"]["team"]["issues"]["nodes"][0]
153
+ issue_text = str(issue["title"])
154
+ issue_id = issue["id"]
155
+ description = issue.get("description", None)
156
+ if description is not None:
157
+ issue_text += "\n" + description
158
+ return issue_text
159
+
160
+ return "Not Todo issue found"
161
+
162
+
163
+ def generate_agent():
164
+ model = SleepPerLastTokenModelLiteLLM(
165
+ max_tokens=250,
166
+ temperature=0.5,
167
+ model_id=model_id,
168
+ api_base="https://api.groq.com/openai/v1/",
169
+ api_key=groq_api_key,
170
+ )
171
+ agent = CodeAgent(
172
+ model=model,
173
+ tools=[get_todo_issue], ## add your tools here (don't remove final answer)
174
+ max_steps=1,
175
+ verbosity_level=1,
176
+ grammar=None,
177
+ planning_interval=None,
178
+ name=None,
179
+ description=None,
180
+ )
181
+ return agent
182
+
183
+
184
+ team_id = None
185
+
186
+
187
+ def update_text():
188
+ def get_team_id(team_name):
189
+ teams_text = """
190
+ query Teams {
191
+ teams {
192
+ nodes {
193
+ id
194
+ name
195
+ }
196
+ }
197
+ }
198
+ """
199
+ result = execute_query("Teams", teams_text, api_key)
200
+ for team in result["data"]["teams"]["nodes"]:
201
+ if team["name"] == team_name:
202
+ return team["id"]
203
+ return None
204
+
205
+ team_name = "Agent"
206
+ global team_id
207
+ global issue_text
208
+ team_id = get_team_id(team_name)
209
+
210
+ if team_id is None:
211
+ return f"Team {team_name} is not found", "Team not found"
212
+ issue_text = "No Issue Found"
213
+ agent_text = "No Agent Advice"
214
+
215
+ agent = generate_agent()
216
+ agent_text = agent.run(
217
+ """
218
+ First, get the Todo using the get_todo tool.
219
+ Then, solve the Todo.
220
+ Finally, return the result of solving the Todo.
221
+ """
222
+ )
223
+
224
+ add_comment(issue_id, model_id, agent_text)
225
+ change_state_reviewing()
226
+ # return "", ""
227
+
228
+ return issue_text, agent_text
229
+
230
+
231
+ with gr.Blocks() as demo:
232
+ gr.HTML("""<h1>Linear.app API and smolagents demo</h1>
233
+ <h2>Prepare</h2>
234
+ <p>Need Linear.app acount and api key</a>
235
+ <p>Remember team name and add "Reviewing" State<p>
236
+ """)
237
+ with gr.Row():
238
+ with gr.Column():
239
+ gr.Markdown("## Issue")
240
+ # issue = gr.Markdown(load_text("issue.md"))
241
+ issue = gr.Markdown("issue")
242
+ with gr.Column():
243
+ gr.Markdown("## Agent advice(Don't trust them completely)")
244
+ # output = gr.Markdown(load_text("output.md"))
245
+ output = gr.Markdown("agent result")
246
+ demo.load(update_text, inputs=None, outputs=[issue, output])
247
+
248
+ #
249
+ # bt = gr.Button("Next Todo")
250
+ # bt.click(update_text, inputs=None, outputs=[issue, output])
251
+
252
+
253
+ if __name__ == "__main__": # without main call twice
254
+ demo.launch()
linear_api_utils.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This code is licensed under the MIT License.
2
+ # Copyright (c) [2025] [Akihito Miyazaki]
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+ #
22
+
23
+ import json
24
+ import os
25
+ import time
26
+
27
+ #
28
+ from pprint import pprint
29
+ import requests
30
+
31
+
32
+ def request_linear(
33
+ headers, data, url="https://api.linear.app/graphql", print_header=False
34
+ ):
35
+ response_data = None
36
+ try:
37
+ response = requests.post(url, headers=headers, json=data)
38
+
39
+ response_data = response.json()
40
+ if print_header:
41
+ print("--- ヘッダーの表示開始 ---")
42
+ pprint(dict(response.headers), indent=4)
43
+ print("--- ヘッダーの表示終了 ---")
44
+
45
+ response.raise_for_status() # ステータスコードが200番台以外の場合に例外を発生させる
46
+ return response_data
47
+ except requests.exceptions.RequestException as e:
48
+ print(response_data)
49
+ print(f"エラーが発生しました: {e}")
50
+ # exit(0)
51
+ except json.JSONDecodeError as e:
52
+ print(f"JSONデコードエラー: {e}")
53
+ print(f"レスポンス内容:\n{response.text}")
54
+ # exit(0)
55
+
56
+
57
+ def load_api_key(dir="./"):
58
+ print(f"{dir}.env")
59
+ from dotenv import load_dotenv
60
+
61
+ load_dotenv(dotenv_path=f"{dir}.env")
62
+ if "api_key" in os.environ:
63
+ api_key = os.environ["api_key"]
64
+ return api_key
65
+ else:
66
+ print("'api_key' が環境変数にありません。")
67
+ print(".envファイルを作成し 以下の行を追加してください。")
68
+ print("api_key=your_api_key")
69
+ print("このファイルは.gitignoreに追加して、決して公開しないでください。")
70
+ print(
71
+ "Linear Settings Security&access - Personal API keysからAPI Keyは作成できます。"
72
+ )
73
+ exit(0)
74
+
75
+
76
+ def execute_query(label, query_text, authorization, print_header=False):
77
+ headers = {
78
+ "Content-Type": "application/json",
79
+ "Authorization": authorization,
80
+ }
81
+
82
+ start_time_total = time.time()
83
+ print(f"--- 処理の開始:{label} ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---")
84
+
85
+ query_dic = {"query": query_text}
86
+ print("--- クエリの表示開始 ---")
87
+ print(f"{query_dic['query']}")
88
+ print("--- クエリの表示終了 ---")
89
+ result = request_linear(headers, query_dic, print_header=print_header)
90
+ print("--- 結果の表示開始 ---")
91
+ print(json.dumps(result, indent=2, ensure_ascii=False))
92
+ print("--- 結果の表示終了 ---")
93
+ end_time_total = time.time()
94
+ total_time = end_time_total - start_time_total
95
+ print(f"--- 処理の終了:{label} ---")
96
+ print(f"合計処理時間: {total_time:.4f} 秒")
97
+
98
+ print("") # spacer
99
+
100
+ return result
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ litellm
2
+ dotenv
3
+ requests
4
+ smolagents
sleep_per_last_token_model.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import List, Optional, Dict
3
+
4
+ from smolagents import OpenAIServerModel, LiteLLMModel, ChatMessage, Tool
5
+
6
+
7
+ class SleepPerLastTokenModelLiteLLM(LiteLLMModel):
8
+ def __init__(self, sleep_factor: float = 0.01, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.sleep_factor = sleep_factor
11
+
12
+ def __call__(
13
+ self,
14
+ messages: List[Dict[str, str]],
15
+ stop_sequences: Optional[List[str]] = None,
16
+ grammar: Optional[str] = None,
17
+ tools_to_call_from: Optional[List[Tool]] = None,
18
+ **kwargs,
19
+ ) -> ChatMessage:
20
+ if self.last_input_token_count is not None:
21
+ sleep_time = (
22
+ self.last_input_token_count + self.last_output_token_count
23
+ ) * self.sleep_factor
24
+ print(f"Sleeping for {sleep_time:.2f} seconds...")
25
+ time.sleep(sleep_time)
26
+
27
+ return super().__call__(
28
+ messages, stop_sequences, grammar, tools_to_call_from, **kwargs
29
+ )
30
+
31
+
32
+ # smolagents 1.9.2 not working
33
+ # Error value must be a string ?
34
+ """
35
+ class SleepPerLastTokenModelOpenAI(OpenAIServerModel):
36
+ def __init__(self, sleep_factor: float = 0.01, **kwargs):
37
+ super().__init__(**kwargs)
38
+ self.sleep_factor = sleep_factor
39
+
40
+ def __call__(
41
+ self,
42
+ messages: List[Dict[str, str]],
43
+ stop_sequences: Optional[List[str]] = None,
44
+ grammar: Optional[str] = None,
45
+ tools_to_call_from: Optional[List[Tool]] = None,
46
+ **kwargs,
47
+ ) -> ChatMessage:
48
+ if self.last_input_token_count is not None:
49
+ sleep_time = self.last_input_token_count * self.sleep_factor
50
+ print(f"Sleeping for {sleep_time:.2f} seconds...")
51
+ time.sleep(sleep_time)
52
+
53
+ return super().__call__(
54
+ messages, stop_sequences, grammar, tools_to_call_from, **kwargs
55
+ )
56
+ """