Akjava commited on
Commit
f15e0a9
·
1 Parent(s): 47488e1
app.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ このコードは以下のコードをLinear.appに対応させたものです。公式のコードではありません。
3
+ https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/_webhooks_server.py
4
+ https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/_webhooks_payload.py
5
+
6
+ まだ、Issue Objectのごく一部しか対応してません。
7
+ https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Issue
8
+
9
+ .envファイルに、api_key = linear-api-key および、webhook_secret = linear-webhook-secretの設定が必要です。
10
+
11
+ ローカルは起動ごとにURLが変わるので、起動時にLinear-APIでURLを更新しています。
12
+ target_webhook_label(デフォルト値はGradio)で指定したラベルのwebhookを実行時に上書きます。
13
+
14
+ gradio,fastapi,pydanticをあらかじめインストールしておく必要があります。
15
+ .envにlinear api_keyおよび、linear-webhook secretを記述する必要があります。
16
+ また、このExampleはUpdateのみなので、最初にラベルGradioでWebhookを作っておいてください。
17
+
18
+ ** Linear.app 対応部分の著作権表示 **
19
+ # Copyright 2025-present, Akihito Miyazaki
20
+ #
21
+ # Licensed under the Apache License, Version 2.0 (the "License");
22
+ # you may not use this file except in compliance with the License.
23
+ # You may obtain a copy of the License at
24
+ #
25
+ # http://www.apache.org/licenses/LICENSE-2.0
26
+ #
27
+ # Unless required by applicable law or agreed to in writing, software
28
+ # distributed under the License is distributed on an "AS IS" BASIS,
29
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30
+ # See the License for the specific language governing permissions and
31
+ # limitations under the License.
32
+
33
+ ** Hugging Face Hub ライブラリのライセンス表示 **
34
+ # Copyright 2023-present, the HuggingFace Inc. team.
35
+ #
36
+ # Licensed under the Apache License, Version 2.0 (the "License");
37
+ # you may not use this file except in compliance with the License.
38
+ # You may obtain a copy of the License at
39
+ #
40
+ # http://www.apache.org/licenses/LICENSE-2.0
41
+ #
42
+ # Unless required by applicable law or agreed to in writing, software
43
+ # distributed under the License is distributed on an "AS IS" BASIS,
44
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
45
+ # See the License for the specific language governing permissions and
46
+ # limitations under the License.
47
+
48
+ このコードには Hugging Face Hub ライブラリの一部が含まれており、Apache License, Version 2.0 の下でライセンスされています。
49
+ ライセンスの全文は以下から確認できます: http://www.apache.org/licenses/LICENSE-2.0
50
+ """
51
+
52
+ import os
53
+ from pprint import pprint
54
+
55
+ import gradio as gr
56
+ from smolagents import CodeAgent, HfApiModel
57
+
58
+ from linear_api_utils import execute_query
59
+ from gradio_webhook_server import WebhooksServer
60
+ from gradio_webhook_payload import WebhookPayload
61
+ from sleep_per_last_token_model import SleepPerLastTokenModelLiteLLM
62
+
63
+
64
+ def getenv(key, is_value_error_on_null=True):
65
+ value = os.getenv(key)
66
+ if value is None:
67
+ from dotenv import load_dotenv
68
+
69
+ load_dotenv()
70
+ value = os.getenv(key)
71
+ if is_value_error_on_null and value is None:
72
+ raise ValueError(f"Need {key} on secret or .env(If running on local)")
73
+ return value
74
+
75
+
76
+ # SETTINGS
77
+ LINEAR_ISSUE_LABEL = "huggingface-public" # only show issue with this label,I added for demo you can remove this
78
+ LINEAR_WEBHOOK_LABEL = "Huggingface" # you have to create in linear before run script
79
+ # set secret key on Space setting or .env(local)
80
+ # hf_token = getenv("HF_TOKEN")
81
+ groq_api_key = getenv("GROQ_API_KEY")
82
+ api_key = getenv("LINEAR_API_KEY")
83
+ webhook_key = getenv("LINEAR_WEBHOOK_KEY")
84
+
85
+
86
+ if api_key is None:
87
+ raise ValueError("Need LINEAR_API_KEY on secret")
88
+ if webhook_key is None:
89
+ raise ValueError("Need LINEAR_WEBHOOK_KEY on secret")
90
+
91
+ webhook_query_text = """
92
+ query {
93
+ webhooks{
94
+ nodes {
95
+ id
96
+ label
97
+ url
98
+ }
99
+ }
100
+ }
101
+ """
102
+ target_webhook_label = LINEAR_WEBHOOK_LABEL # filter not working,set manual
103
+ target_webhook_id = None
104
+ result = execute_query("webhook", webhook_query_text, api_key)
105
+ for webhook in result["data"]["webhooks"]["nodes"]:
106
+ if target_webhook_label == webhook["label"]:
107
+ target_webhook_id = webhook["id"]
108
+
109
+
110
+ app = None
111
+
112
+ model = SleepPerLastTokenModelLiteLLM(
113
+ max_tokens=100,
114
+ temperature=0.5,
115
+ model_id="groq/llama3-8b-8192",
116
+ api_base="https://api.groq.com/openai/v1/",
117
+ api_key=groq_api_key,
118
+ )
119
+
120
+ """
121
+ model = HfApiModel(
122
+ max_tokens=100,
123
+ temperature=0.5,
124
+ model_id="google/gemma-2-2b-it",
125
+ custom_role_conversions=None,
126
+ token=hf_token,
127
+ )
128
+ """
129
+
130
+ agent = CodeAgent(
131
+ model=model,
132
+ tools=[], ## add your tools here (don't remove final answer)
133
+ max_steps=1,
134
+ verbosity_level=1,
135
+ grammar=None,
136
+ planning_interval=None,
137
+ name=None,
138
+ description=None,
139
+ )
140
+
141
+
142
+ def update():
143
+ result = agent.run(f"how to solve this issue:{app.text}")
144
+ return app.text, result
145
+
146
+
147
+ with gr.Blocks() as ui:
148
+ gr.HTML("""<h1>Linear.app Webhook Server</h1>
149
+ <p>This is Demo of Direct Webhook-triggered AIAgen</p>
150
+ <p>it's still just simple code,sadly you have to click to show updates.</p>
151
+ <p><b>Imagine an agent, responding instantly.</b></p>
152
+ <p>Technically Gradio have no way to update without action<p>
153
+ <p></p><br>
154
+ <p>I'm confused by Hugging Face's new pricing system. I'm worried about potentially massive inference API bills, so I switched to Groq.</p>
155
+ <p>I believe my use of the Groq API is currently compliant with Hugging Face's Content Policy.</p>
156
+ <p>If you have any questions, please disable the Space or contact me before taking any action against my account. Thank you for your understanding.</p>
157
+ """)
158
+ with gr.Row():
159
+ issue_box = gr.Textbox(label="Issue")
160
+ output_box = gr.Textbox(label="出力")
161
+ bt = gr.Button("Ask AI")
162
+ bt.click(update, outputs=[issue_box, output_box])
163
+
164
+ app = WebhooksServer(
165
+ ui=ui,
166
+ webhook_secret=webhook_key, # loaded by load_api_key
167
+ )
168
+ app.text = "nothing"
169
+
170
+
171
+ @app.add_webhook("/linear_webhook")
172
+ async def updated(payload: WebhookPayload):
173
+ pprint(payload.dict(), indent=4)
174
+
175
+ data = payload.dict()["data"]
176
+ has_label = True
177
+ if LINEAR_ISSUE_LABEL:
178
+ has_label = False
179
+ for label in data["labels"]:
180
+ if label["name"] == LINEAR_ISSUE_LABEL:
181
+ has_label = True
182
+
183
+ if has_label:
184
+ text = data["description"]
185
+ app.text = text
186
+ return {"message": "ok"}
187
+
188
+
189
+ def webhook_update(url):
190
+ webhook_update_text = """
191
+ mutation {
192
+ webhookUpdate(
193
+ id: "%s"
194
+ input:{
195
+ url:"%s"
196
+ }
197
+ ) {
198
+ success
199
+ }
200
+ }
201
+ """ % (target_webhook_id, url)
202
+ result = execute_query("webhook_update", webhook_update_text, api_key)
203
+
204
+
205
+ app.launch(webhook_update=webhook_update)
gradio_webhook_payload.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ このコードは以下のコードをLinear.appに対応させたものです。公式のコードではありません。
3
+ https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/_webhooks_payload.py
4
+
5
+ まだ、Issue Objectのごく一部しか対応してません。実際には、設定次第でissue以外のデーターも飛んでくるようになりますが、対応してません。(Unionとか使うぽい)
6
+ https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Issue
7
+
8
+ Issueの新規・更新・削除は確認しました。
9
+
10
+ そして、変更が激しい部分なので、将来 属性が、deprecatedからremovedになりエラーが出るかもしれません。
11
+
12
+
13
+ ** Linear.app 対応部分の著作権表示 **
14
+ # Copyright 2025-present, Akihito Miyazaki
15
+ #
16
+ # Licensed under the Apache License, Version 2.0 (the "License");
17
+ # you may not use this file except in compliance with the License.
18
+ # You may obtain a copy of the License at
19
+ #
20
+ # http://www.apache.org/licenses/LICENSE-2.0
21
+ #
22
+ # Unless required by applicable law or agreed to in writing, software
23
+ # distributed under the License is distributed on an "AS IS" BASIS,
24
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
+ # See the License for the specific language governing permissions and
26
+ # limitations under the License.
27
+
28
+ ** Hugging Face Hub ライブラリのライセンス表示 **
29
+ # Copyright 2023-present, the HuggingFace Inc. team.
30
+ #
31
+ # Licensed under the Apache License, Version 2.0 (the "License");
32
+ # you may not use this file except in compliance with the License.
33
+ # You may obtain a copy of the License at
34
+ #
35
+ # http://www.apache.org/licenses/LICENSE-2.0
36
+ #
37
+ # Unless required by applicable law or agreed to in writing, software
38
+ # distributed under the License is distributed on an "AS IS" BASIS,
39
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
40
+ # See the License for the specific language governing permissions and
41
+ # limitations under the License.
42
+
43
+ このコードには Hugging Face Hub ライブラリの一部が含まれており、Apache License, Version 2.0 の下でライセンスされています。
44
+ ライセンスの全文は以下から確認できます: http://www.apache.org/licenses/LICENSE-2.0
45
+ """
46
+
47
+ """Contains data structures to parse the webhooks payload."""
48
+
49
+ from typing import List, Literal, Optional
50
+ from datetime import datetime
51
+
52
+
53
+ def is_pydantic_available():
54
+ return True
55
+
56
+
57
+ if is_pydantic_available():
58
+ from pydantic import BaseModel
59
+ else:
60
+ # Define a dummy BaseModel to avoid import errors when pydantic is not installed
61
+ # Import error will be raised when trying to use the class
62
+
63
+ class BaseModel: # type: ignore [no-redef]
64
+ def __init__(self, *args, **kwargs) -> None:
65
+ raise ImportError(
66
+ "You must have `pydantic` installed to use `WebhookPayload`. This is an optional dependency that"
67
+ " should be installed separately. Please run `pip install --upgrade pydantic` and retry."
68
+ )
69
+
70
+
71
+ # This is an adaptation of the ReportV3 interface implemented in moon-landing. V0, V1 and V2 have been ignored as they
72
+ # are not in used anymore. To keep in sync when format is updated in
73
+ # https://github.com/huggingface/moon-landing/blob/main/server/lib/HFWebhooks.ts (internal link).
74
+
75
+
76
+ class WebhookPayloadUploadFrom(BaseModel):
77
+ stateId: Optional[str] = None
78
+ updatedAt: datetime
79
+ description: Optional[str] = None
80
+
81
+
82
+ class WebhookPayloadTeam(BaseModel):
83
+ id: str
84
+ name: str
85
+ key: str
86
+
87
+
88
+ class WebhookPayloadProject(BaseModel):
89
+ id: str
90
+ name: str
91
+ url: str
92
+
93
+
94
+ class WebhookPayloadState(BaseModel):
95
+ id: str
96
+ color: str
97
+ name: str
98
+ type: str
99
+
100
+
101
+ class WebhookPayloadLabel(BaseModel):
102
+ id: str
103
+ color: str
104
+ name: str
105
+
106
+
107
+ class WebhookPayloadData(BaseModel):
108
+ id: str
109
+ createdAt: datetime
110
+ updatedAt: datetime
111
+ archivedAt: Optional[datetime] = None
112
+ title: str
113
+ description: Optional[str] = None
114
+ labels: List[WebhookPayloadLabel] = []
115
+ priority: int
116
+ estimate: Optional[int] = None
117
+ startedAt: Optional[datetime] = None
118
+ state: WebhookPayloadState
119
+ team: WebhookPayloadTeam
120
+ project: Optional[WebhookPayloadProject] = None
121
+
122
+
123
+ class WebhookPayload(BaseModel):
124
+ action: str
125
+ type: str
126
+ createdAt: str
127
+ data: WebhookPayloadData
128
+ url: Optional[str] = None
129
+ webhookTimestamp: int
130
+ webhookId: str
131
+ updatedFrom: Optional[WebhookPayloadUploadFrom] = None
gradio_webhook_server.py ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ このコードは以下のコードをLinear.appに対応させたものです。公式のコードではありません。
3
+ Local Gradioの動作のみ確認・Spaceでのテストはまだしていません。
4
+ 特に verify_signature 関数と、webhook 呼び出し時の request ヘッダーの取り扱いを変更しています。
5
+ https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/_webhooks_server.py
6
+
7
+ なおリクエストのデバッグのため以下に上書きで書き出しを行っています。
8
+ webhook_header.json
9
+ webhook_request.json
10
+
11
+ ** Linear.app 対応部分の著作権表示 **
12
+ # Copyright 2025-present, Akihito Miyazaki
13
+ #
14
+ # Licensed under the Apache License, Version 2.0 (the "License");
15
+ # you may not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing, software
21
+ # distributed under the License is distributed on an "AS IS" BASIS,
22
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
+ # See the License for the specific language governing permissions and
24
+ # limitations under the License.
25
+
26
+ ** Hugging Face Hub ライブラリのライセンス表示 **
27
+ # Copyright 2023-present, the HuggingFace Inc. team.
28
+ #
29
+ # Licensed under the Apache License, Version 2.0 (the "License");
30
+ # you may not use this file except in compliance with the License.
31
+ # You may obtain a copy of the License at
32
+ #
33
+ # http://www.apache.org/licenses/LICENSE-2.0
34
+ #
35
+ # Unless required by applicable law or agreed to in writing, software
36
+ # distributed under the License is distributed on an "AS IS" BASIS,
37
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38
+ # See the License for the specific language governing permissions and
39
+ # limitations under the License.
40
+
41
+ このコードには Hugging Face Hub ライブラリの一部が含まれており、Apache License, Version 2.0 の下でライセンスされています。
42
+ ライセンスの全文は以下から確認できます: http://www.apache.org/licenses/LICENSE-2.0
43
+ """
44
+
45
+ """Contains `WebhooksServer` and `webhook_endpoint` to create a webhook server easily."""
46
+
47
+ import atexit
48
+ import inspect
49
+ import os
50
+ from functools import wraps
51
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
52
+
53
+ import hashlib
54
+ import hmac
55
+ import json
56
+ from fastapi.encoders import jsonable_encoder
57
+
58
+
59
+ def verify_signature(request_headers, payload, webhook_secret):
60
+ """
61
+ リクエストヘッダーの署名と、ペイロードから生成した署名を比較して、リクエストが有効かどうかを検証します。
62
+
63
+ Args:
64
+ request_headers: リクエストヘッダー (dict-like object, e.g., `request.headers`)
65
+ payload: リクエストのペイロード (bytes or string)
66
+ webhook_secret: ウェブフックのシークレットキー (string)
67
+
68
+ Returns:
69
+ True: 署名が一致する場合 (リクエストは有効)
70
+ False: 署名が一致しない場合 (リクエストは無効)
71
+ """
72
+
73
+ # 環境変数からWEBHOOK_SECRETを取得(Netlify.env.get('WEBHOOK_SECRET')の代替)
74
+ # 例:webhook_secret = os.environ.get('WEBHOOK_SECRET')
75
+ # 実際の環境変数名に合わせてください
76
+
77
+ # ペイロードが文字列ならバイト列に変換
78
+ if isinstance(payload, str):
79
+ payload = payload.encode("utf-8")
80
+
81
+ # HMAC署名を生成
82
+ signature = hmac.new(
83
+ webhook_secret.encode("utf-8"), # シークレットキーはバイト列にする
84
+ payload,
85
+ hashlib.sha256,
86
+ ).hexdigest()
87
+
88
+ # リクエストヘッダーから署名を取得
89
+ # ヘッダー名は大文字小文字を区別しない場合があるので注意
90
+ linear_signature = request_headers.get("linear-signature")
91
+ if not linear_signature:
92
+ linear_signature = request_headers.get(
93
+ "Linear-Signature"
94
+ ) # ヘッダー名の大文字小文字のバリエーションもチェック
95
+ if not linear_signature:
96
+ print("Error: linear-signature header not found")
97
+ return False
98
+
99
+ # 署名を比較
100
+ return hmac.compare_digest(signature, linear_signature) # 脆弱性対策
101
+
102
+
103
+ # from .utils import experimental, is_fastapi_available, is_gradio_available
104
+ # skip check
105
+ def is_fastapi_available():
106
+ return True
107
+
108
+
109
+ def is_gradio_available():
110
+ return True
111
+
112
+
113
+ if TYPE_CHECKING:
114
+ import gradio as gr
115
+ from fastapi import Request
116
+
117
+ if is_fastapi_available():
118
+ from fastapi import FastAPI, Request
119
+ from fastapi.responses import JSONResponse
120
+ else:
121
+ # Will fail at runtime if FastAPI is not available
122
+ FastAPI = Request = JSONResponse = None # type: ignore [misc, assignment]
123
+
124
+
125
+ _global_app: Optional["WebhooksServer"] = None
126
+ _is_local = os.environ.get("SPACE_ID") is None
127
+
128
+
129
+ class WebhooksServer:
130
+ """
131
+ The [`WebhooksServer`] class lets you create an instance of a Gradio app that can receive Huggingface webhooks.
132
+ These webhooks can be registered using the [`~WebhooksServer.add_webhook`] decorator. Webhook endpoints are added to
133
+ the app as a POST endpoint to the FastAPI router. Once all the webhooks are registered, the `launch` method has to be
134
+ called to start the app.
135
+
136
+ It is recommended to accept [`WebhookPayload`] as the first argument of the webhook function. It is a Pydantic
137
+ model that contains all the information about the webhook event. The data will be parsed automatically for you.
138
+
139
+ Check out the [webhooks guide](../guides/webhooks_server) for a step-by-step tutorial on how to setup your
140
+ WebhooksServer and deploy it on a Space.
141
+
142
+ <Tip warning={true}>
143
+
144
+ `WebhooksServer` is experimental. Its API is subject to change in the future.
145
+
146
+ </Tip>
147
+
148
+ <Tip warning={true}>
149
+
150
+ You must have `gradio` installed to use `WebhooksServer` (`pip install --upgrade gradio`).
151
+
152
+ </Tip>
153
+
154
+ Args:
155
+ ui (`gradio.Blocks`, optional):
156
+ A Gradio UI instance to be used as the Space landing page. If `None`, a UI displaying instructions
157
+ about the configured webhooks is created.
158
+ webhook_secret (`str`, optional):
159
+ A secret key to verify incoming webhook requests. You can set this value to any secret you want as long as
160
+ you also configure it in your [webhooks settings panel](https://huggingface.co/settings/webhooks). You
161
+ can also set this value as the `WEBHOOK_SECRET` environment variable. If no secret is provided, the
162
+ webhook endpoints are opened without any security.
163
+
164
+ Example:
165
+
166
+ ```python
167
+ import gradio as gr
168
+ from huggingface_hub import WebhooksServer, WebhookPayload
169
+
170
+ with gr.Blocks() as ui:
171
+ ...
172
+
173
+ app = WebhooksServer(ui=ui, webhook_secret="my_secret_key")
174
+
175
+ @app.add_webhook("/say_hello")
176
+ async def hello(payload: WebhookPayload):
177
+ return {"message": "hello"}
178
+
179
+ app.launch()
180
+ ```
181
+ """
182
+
183
+ def __new__(cls, *args, **kwargs) -> "WebhooksServer":
184
+ if not is_gradio_available():
185
+ raise ImportError(
186
+ "You must have `gradio` installed to use `WebhooksServer`. Please run `pip install --upgrade gradio`"
187
+ " first."
188
+ )
189
+ if not is_fastapi_available():
190
+ raise ImportError(
191
+ "You must have `fastapi` installed to use `WebhooksServer`. Please run `pip install --upgrade fastapi`"
192
+ " first."
193
+ )
194
+ return super().__new__(cls)
195
+
196
+ def __init__(
197
+ self,
198
+ ui: Optional["gr.Blocks"] = None,
199
+ webhook_secret: Optional[str] = None,
200
+ ) -> None:
201
+ self._ui = ui
202
+
203
+ self.webhook_secret = webhook_secret or os.getenv("WEBHOOK_SECRET")
204
+ self.registered_webhooks: Dict[str, Callable] = {}
205
+ _warn_on_empty_secret(self.webhook_secret)
206
+
207
+ def add_webhook(self, path: Optional[str] = None) -> Callable:
208
+ """
209
+ Decorator to add a webhook to the [`WebhooksServer`] server.
210
+
211
+ Args:
212
+ path (`str`, optional):
213
+ The URL path to register the webhook function. If not provided, the function name will be used as the
214
+ path. In any case, all webhooks are registered under `/webhooks`.
215
+
216
+ Raises:
217
+ ValueError: If the provided path is already registered as a webhook.
218
+
219
+ Example:
220
+ ```python
221
+ from huggingface_hub import WebhooksServer, WebhookPayload
222
+
223
+ app = WebhooksServer()
224
+
225
+ @app.add_webhook
226
+ async def trigger_training(payload: WebhookPayload):
227
+ if payload.repo.type == "dataset" and payload.event.action == "update":
228
+ # Trigger a training job if a dataset is updated
229
+ ...
230
+
231
+ app.launch()
232
+ ```
233
+ """
234
+ # Usage: directly as decorator. Example: `@app.add_webhook`
235
+ if callable(path):
236
+ # If path is a function, it means it was used as a decorator without arguments
237
+ return self.add_webhook()(path)
238
+
239
+ # Usage: provide a path. Example: `@app.add_webhook(...)`
240
+ @wraps(FastAPI.post)
241
+ def _inner_post(*args, **kwargs):
242
+ func = args[0]
243
+ abs_path = f"/webhooks/{(path or func.__name__).strip('/')}"
244
+ if abs_path in self.registered_webhooks:
245
+ raise ValueError(f"Webhook {abs_path} already exists.")
246
+ self.registered_webhooks[abs_path] = func
247
+
248
+ return _inner_post
249
+
250
+ def launch(
251
+ self,
252
+ prevent_thread_lock: bool = False,
253
+ webhook_update=None,
254
+ **launch_kwargs: Any,
255
+ ) -> None:
256
+ """Launch the Gradio app and register webhooks to the underlying FastAPI server.
257
+
258
+ Input parameters are forwarded to Gradio when launching the app.
259
+ """
260
+ ui = self._ui or self._get_default_ui()
261
+
262
+ # Start Gradio App
263
+ # - as non-blocking so that webhooks can be added afterwards
264
+ # - as shared if launch locally (to debug webhooks)
265
+ launch_kwargs.setdefault("share", _is_local)
266
+ self.fastapi_app, _, _ = ui.launch(prevent_thread_lock=True, **launch_kwargs)
267
+
268
+ # Register webhooks to FastAPI app
269
+ for path, func in self.registered_webhooks.items():
270
+ # Add secret check if required
271
+ if self.webhook_secret is not None:
272
+ func = _wrap_webhook_to_check_secret(
273
+ func, webhook_secret=self.webhook_secret
274
+ )
275
+
276
+ # Add route to FastAPI app
277
+ self.fastapi_app.post(path)(func)
278
+
279
+ # Print instructions and block main thread
280
+ space_host = os.environ.get("SPACE_HOST")
281
+ url = (
282
+ "https://" + space_host
283
+ if space_host is not None
284
+ else (ui.share_url or ui.local_url)
285
+ )
286
+ url = url.strip("/")
287
+ message = "\nWebhooks are correctly setup and ready to use:"
288
+ message += "\n" + "\n".join(
289
+ f" - POST {url}{webhook}" for webhook in self.registered_webhooks
290
+ )
291
+ message += (
292
+ "\nGo to https://huggingface.co/settings/webhooks to setup your webhooks."
293
+ )
294
+ # print(message)
295
+ gradio_url = f"{url}{next(iter(self.registered_webhooks))}"
296
+ # print(gradio_url)
297
+ webhook_update(gradio_url)
298
+
299
+ if not prevent_thread_lock:
300
+ ui.block_thread()
301
+
302
+ def _get_default_ui(self) -> "gr.Blocks":
303
+ """Default UI if not provided (lists webhooks and provides basic instructions)."""
304
+ import gradio as gr
305
+
306
+ with gr.Blocks() as ui:
307
+ gr.Markdown("# This is an app to process 🤗 Webhooks")
308
+ gr.Markdown(
309
+ "Webhooks are a foundation for MLOps-related features. They allow you to listen for new changes on"
310
+ " specific repos or to all repos belonging to particular set of users/organizations (not just your"
311
+ " repos, but any repo). Check out this [guide](https://huggingface.co/docs/hub/webhooks) to get to"
312
+ " know more about webhooks on the Huggingface Hub."
313
+ )
314
+ gr.Markdown(
315
+ f"{len(self.registered_webhooks)} webhook(s) are registered:"
316
+ + "\n\n"
317
+ + "\n ".join(
318
+ f"- [{webhook_path}]({_get_webhook_doc_url(webhook.__name__, webhook_path)})"
319
+ for webhook_path, webhook in self.registered_webhooks.items()
320
+ )
321
+ )
322
+ gr.Markdown(
323
+ "Go to https://huggingface.co/settings/webhooks to setup your webhooks."
324
+ + "\nYou app is running locally. Please look at the logs to check the full URL you need to set."
325
+ if _is_local
326
+ else (
327
+ "\nThis app is running on a Space. You can find the corresponding URL in the options menu"
328
+ " (top-right) > 'Embed the Space'. The URL looks like 'https://{username}-{repo_name}.hf.space'."
329
+ )
330
+ )
331
+ return ui
332
+
333
+
334
+ def webhook_endpoint(path: Optional[str] = None) -> Callable:
335
+ """Decorator to start a [`WebhooksServer`] and register the decorated function as a webhook endpoint.
336
+
337
+ This is a helper to get started quickly. If you need more flexibility (custom landing page or webhook secret),
338
+ you can use [`WebhooksServer`] directly. You can register multiple webhook endpoints (to the same server) by using
339
+ this decorator multiple times.
340
+
341
+ Check out the [webhooks guide](../guides/webhooks_server) for a step-by-step tutorial on how to setup your
342
+ server and deploy it on a Space.
343
+
344
+ <Tip warning={true}>
345
+
346
+ `webhook_endpoint` is experimental. Its API is subject to change in the future.
347
+
348
+ </Tip>
349
+
350
+ <Tip warning={true}>
351
+
352
+ You must have `gradio` installed to use `webhook_endpoint` (`pip install --upgrade gradio`).
353
+
354
+ </Tip>
355
+
356
+ Args:
357
+ path (`str`, optional):
358
+ The URL path to register the webhook function. If not provided, the function name will be used as the path.
359
+ In any case, all webhooks are registered under `/webhooks`.
360
+
361
+ Examples:
362
+ The default usage is to register a function as a webhook endpoint. The function name will be used as the path.
363
+ The server will be started automatically at exit (i.e. at the end of the script).
364
+
365
+ ```python
366
+ from huggingface_hub import webhook_endpoint, WebhookPayload
367
+
368
+ @webhook_endpoint
369
+ async def trigger_training(payload: WebhookPayload):
370
+ if payload.repo.type == "dataset" and payload.event.action == "update":
371
+ # Trigger a training job if a dataset is updated
372
+ ...
373
+
374
+ # Server is automatically started at the end of the script.
375
+ ```
376
+
377
+ Advanced usage: register a function as a webhook endpoint and start the server manually. This is useful if you
378
+ are running it in a notebook.
379
+
380
+ ```python
381
+ from huggingface_hub import webhook_endpoint, WebhookPayload
382
+
383
+ @webhook_endpoint
384
+ async def trigger_training(payload: WebhookPayload):
385
+ if payload.repo.type == "dataset" and payload.event.action == "update":
386
+ # Trigger a training job if a dataset is updated
387
+ ...
388
+
389
+ # Start the server manually
390
+ trigger_training.launch()
391
+ ```
392
+ """
393
+ if callable(path):
394
+ # If path is a function, it means it was used as a decorator without arguments
395
+ return webhook_endpoint()(path)
396
+
397
+ @wraps(WebhooksServer.add_webhook)
398
+ def _inner(func: Callable) -> Callable:
399
+ app = _get_global_app()
400
+ app.add_webhook(path)(func)
401
+ if len(app.registered_webhooks) == 1:
402
+ # Register `app.launch` to run at exit (only once)
403
+ atexit.register(app.launch)
404
+
405
+ @wraps(app.launch)
406
+ def _launch_now():
407
+ # Run the app directly (without waiting atexit)
408
+ atexit.unregister(app.launch)
409
+ app.launch()
410
+
411
+ func.launch = _launch_now # type: ignore
412
+ return func
413
+
414
+ return _inner
415
+
416
+
417
+ def _get_global_app() -> WebhooksServer:
418
+ global _global_app
419
+ if _global_app is None:
420
+ _global_app = WebhooksServer()
421
+ return _global_app
422
+
423
+
424
+ def _warn_on_empty_secret(webhook_secret: Optional[str]) -> None:
425
+ if webhook_secret is None:
426
+ print(
427
+ "Webhook secret is not defined. This means your webhook endpoints will be open to everyone."
428
+ )
429
+ print(
430
+ "To add a secret, set `WEBHOOK_SECRET` as environment variable or pass it at initialization: "
431
+ "\n\t`app = WebhooksServer(webhook_secret='my_secret', ...)`"
432
+ )
433
+ print(
434
+ "For more details about webhook secrets, please refer to"
435
+ " https://huggingface.co/docs/hub/webhooks#webhook-secret."
436
+ )
437
+ else:
438
+ print("Webhook secret is correctly defined.")
439
+
440
+
441
+ def _get_webhook_doc_url(webhook_name: str, webhook_path: str) -> str:
442
+ """Returns the anchor to a given webhook in the docs (experimental)"""
443
+ return "/docs#/default/" + webhook_name + webhook_path.replace("/", "_") + "_post"
444
+
445
+
446
+ def _wrap_webhook_to_check_secret(func: Callable, webhook_secret: str) -> Callable:
447
+ """Wraps a webhook function to check the webhook secret before calling the function.
448
+
449
+ This is a hacky way to add the `request` parameter to the function signature. Since FastAPI based itself on route
450
+ parameters to inject the values to the function, we need to hack the function signature to retrieve the `Request`
451
+ object (and hence the headers). A far cleaner solution would be to use a middleware. However, since
452
+ `fastapi==0.90.1`, a middleware cannot be added once the app has started. And since the FastAPI app is started by
453
+ Gradio internals (and not by us), we cannot add a middleware.
454
+
455
+ This method is called only when a secret has been defined by the user. If a request is sent without the
456
+ "x-webhook-secret", the function will return a 401 error (unauthorized). If the header is sent but is incorrect,
457
+ the function will return a 403 error (forbidden).
458
+
459
+ Inspired by https://stackoverflow.com/a/33112180.
460
+ """
461
+ initial_sig = inspect.signature(func)
462
+
463
+ @wraps(func)
464
+ async def _protected_func(request: Request, **kwargs):
465
+ request_text = await request.body()
466
+
467
+ data = json.loads(request_text.decode())
468
+
469
+ # ファイルに保存(インデント付き)
470
+ with open("webhook_header.json", "w", encoding="utf-8") as f:
471
+ serialized = jsonable_encoder(request.headers)
472
+ json.dump(serialized, f, indent=2, ensure_ascii=False)
473
+ with open("webhook_request.json", "w", encoding="utf-8") as f:
474
+ json.dump(data, f, indent=2, ensure_ascii=False)
475
+
476
+ result = verify_signature(request.headers, request_text, webhook_secret)
477
+ if not result:
478
+ print("invalid signature")
479
+ return JSONResponse({"error": "Invalid webhook secret."}, status_code=403)
480
+
481
+ # Inject `request` in kwargs if required
482
+ if "request" in initial_sig.parameters:
483
+ kwargs["request"] = request
484
+
485
+ # Handle both sync and async routes
486
+ if inspect.iscoroutinefunction(func):
487
+ return await func(**kwargs)
488
+ else:
489
+ return func(**kwargs)
490
+
491
+ # Update signature to include request
492
+ if "request" not in initial_sig.parameters:
493
+ _protected_func.__signature__ = initial_sig.replace( # type: ignore
494
+ parameters=(
495
+ inspect.Parameter(
496
+ name="request",
497
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
498
+ annotation=Request,
499
+ ),
500
+ )
501
+ + tuple(initial_sig.parameters.values())
502
+ )
503
+
504
+ # Return protected route
505
+ return _protected_func
linear_api_utils.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # from dotenv import load_dotenv
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
+ load_dotenv(dotenv_path=f"{dir}.env")
60
+ if "api_key" in os.environ:
61
+ api_key = os.environ["api_key"]
62
+ return api_key
63
+ else:
64
+ print("'api_key' が環境変数にありません。")
65
+ print(".envファイルを作成し 以下の行を追加してください。")
66
+ print("api_key=your_api_key")
67
+ print("このファイルは.gitignoreに追加して、決して公開しないでください。")
68
+ print(
69
+ "Linear Settings Security&access - Personal API keysからAPI Keyは作成できます。"
70
+ )
71
+ exit(0)
72
+
73
+
74
+ def execute_query(label, query_text, authorization, print_header=False):
75
+ headers = {
76
+ "Content-Type": "application/json",
77
+ "Authorization": authorization,
78
+ }
79
+
80
+ start_time_total = time.time()
81
+ print(f"--- 処理の開始:{label} ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---")
82
+
83
+ query_dic = {"query": query_text}
84
+ print("--- クエリの表示開始 ---")
85
+ print(f"{query_dic['query']}")
86
+ print("--- クエリの表示終了 ---")
87
+ result = request_linear(headers, query_dic, print_header=print_header)
88
+ print("--- 結果の表示開始 ---")
89
+ print(json.dumps(result, indent=2, ensure_ascii=False))
90
+ print("--- 結果の表示終了 ---")
91
+ end_time_total = time.time()
92
+ total_time = end_time_total - start_time_total
93
+ print(f"--- 処理の終了:{label} ---")
94
+ print(f"合計処理時間: {total_time:.4f} 秒")
95
+
96
+ print("") # spacer
97
+
98
+ return result
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ smolagents
2
+ dotenv
3
+ requests
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
+ """