diff --git a/config.yaml b/config.yaml index adf3ada..0b7086d 100644 --- a/config.yaml +++ b/config.yaml @@ -11,7 +11,7 @@ typescript: java: source_root: core/src/main/java/com/google/adk exclude: - - examples + - com.example go: source_root: . diff --git a/playground.ipynb b/playground.ipynb index 9fe9688..6620e67 100644 --- a/playground.ipynb +++ b/playground.ipynb @@ -2,10 +2,41 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 15, "id": "02bd4661-609e-40c1-95e7-3f4fc946c69b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[autoreload of google.adk.scope.features_pb2 failed: Traceback (most recent call last):\n", + " File \"/Users/shahins/.pyenv/versions/3.12.4/envs/main/lib/python3.12/site-packages/IPython/extensions/autoreload.py\", line 325, in check\n", + " superreload(m, reload, self.old_objects)\n", + " File \"/Users/shahins/.pyenv/versions/3.12.4/envs/main/lib/python3.12/site-packages/IPython/extensions/autoreload.py\", line 580, in superreload\n", + " module = reload(module)\n", + " ^^^^^^^^^^^^^^\n", + " File \"/Users/shahins/.pyenv/versions/3.12.4/lib/python3.12/importlib/__init__.py\", line 131, in reload\n", + " _bootstrap._exec(spec, module)\n", + " File \"\", line 866, in _exec\n", + " File \"\", line 995, in exec_module\n", + " File \"\", line 488, in _call_with_frames_removed\n", + " File \"/Users/shahins/projects/adk/adk-scope/src/google/adk/scope/features_pb2.py\", line 27, in \n", + " DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\\n\\x0e\\x66\\x65\\x61tures.proto\\x12\\x0fgoogle.adk.meta\\\"\\xc4\\x01\\n\\x05Param\\x12\\x15\\n\\roriginal_name\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0fnormalized_name\\x18\\x02 \\x01(\\t\\x12\\x16\\n\\x0eoriginal_types\\x18\\x03 \\x03(\\t\\x12\\x34\\n\\x10normalized_types\\x18\\x04 \\x03(\\x0e\\x32\\x1a.google.adk.meta.ParamType\\x12\\x18\\n\\x0b\\x64\\x65scription\\x18\\x05 \\x01(\\tH\\x00\\x88\\x01\\x01\\x12\\x13\\n\\x0bis_optional\\x18\\x06 \\x01(\\x08\\x42\\x0e\\n\\x0c_description\\\"\\xdc\\x04\\n\\x07\\x46\\x65\\x61ture\\x12\\x15\\n\\roriginal_name\\x18\\x01 \\x01(\\t\\x12\\x17\\n\\x0fnormalized_name\\x18\\x02 \\x01(\\t\\x12\\x18\\n\\x0b\\x64\\x65scription\\x18\\x03 \\x01(\\tH\\x00\\x88\\x01\\x01\\x12\\x11\\n\\tmember_of\\x18\\x04 \\x01(\\t\\x12\\x1c\\n\\x14normalized_member_of\\x18\\x05 \\x01(\\t\\x12\\x38\\n\\x08maturity\\x18\\x06 \\x01(\\x0e\\x32!.google.adk.meta.Feature.MaturityH\\x01\\x88\\x01\\x01\\x12+\\n\\x04type\\x18\\x07 \\x01(\\x0e\\x32\\x1d.google.adk.meta.Feature.Type\\x12\\x11\\n\\tfile_path\\x18\\x08 \\x01(\\t\\x12\\x11\\n\\tnamespace\\x18\\t \\x01(\\t\\x12\\x1c\\n\\x14normalized_namespace\\x18\\n \\x01(\\t\\x12*\\n\\nparameters\\x18\\x0b \\x03(\\x0b\\x32\\x16.google.adk.meta.Param\\x12\\x1d\\n\\x15original_return_types\\x18\\x0c \\x03(\\t\\x12\\x1f\\n\\x17normalized_return_types\\x18\\r \\x03(\\t\\x12\\x12\\n\\x05\\x61sync\\x18\\x0e \\x01(\\x08H\\x02\\x88\\x01\\x01\\\"6\\n\\x08Maturity\\x12\\x10\\n\\x0c\\x45XPERIMENTAL\\x10\\x00\\x12\\x08\\n\\x04\\x42\\x45TA\\x10\\x01\\x12\\x0e\\n\\nDEPRECATED\\x10\\x02\\\"L\\n\\x04Type\\x12\\x0c\\n\\x08\\x46UNCTION\\x10\\x00\\x12\\x13\\n\\x0fINSTANCE_METHOD\\x10\\x01\\x12\\x10\\n\\x0c\\x43LASS_METHOD\\x10\\x02\\x12\\x0f\\n\\x0b\\x43ONSTRUCTOR\\x10\\x03\\x42\\x0e\\n\\x0c_descriptionB\\x0b\\n\\t_maturityB\\x08\\n\\x06_async\\\"s\\n\\x0f\\x46\\x65\\x61tureRegistry\\x12\\x10\\n\\x08language\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07version\\x18\\x02 \\x01(\\t\\x12\\x11\\n\\tcommit_id\\x18\\x03 \\x01(\\t\\x12*\\n\\x08\\x66\\x65\\x61tures\\x18\\x04 \\x03(\\x0b\\x32\\x18.google.adk.meta.Feature*o\\n\\tParamType\\x12\\n\\n\\x06OBJECT\\x10\\x00\\x12\\n\\n\\x06STRING\\x10\\x01\\x12\\n\\n\\x06NUMBER\\x10\\x02\\x12\\x0b\\n\\x07\\x42OOLEAN\\x10\\x03\\x12\\x08\\n\\x04LIST\\x10\\x04\\x12\\x07\\n\\x03MAP\\x10\\x05\\x12\\x07\\n\\x03SET\\x10\\x06\\x12\\x0b\\n\\x07UNKNOWN\\x10\\x07\\x12\\x08\\n\\x04NULL\\x10\\x08\\x62\\x06proto3')\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "TypeError: Couldn't build proto file into descriptor pool: duplicate file name features.proto\n", + "]\n" + ] + } + ], "source": [ "import sys\n", "import os\n", @@ -30,17 +61,17 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 16, "id": "96c6f1de-6442-4d9b-b21a-06dcacebff69", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "((1426, 10), (1426, 10), (1426, 10))" + "((1578, 10), (2377, 10), (1460, 10))" ] }, - "execution_count": 3, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -55,25 +86,25 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 17, "id": "093a3f6c-131a-42be-aa61-0c1b8209c009", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "count 1426.000000\n", - "mean 0.409128\n", - "std 0.152272\n", + "count 1578.000000\n", + "mean 0.291746\n", + "std 0.167895\n", "min 0.000000\n", - "25% 0.349225\n", - "50% 0.407950\n", - "75% 0.476100\n", - "max 0.992100\n", + "25% 0.231607\n", + "50% 0.293370\n", + "75% 0.381544\n", + "max 0.985000\n", "Name: score, dtype: float64" ] }, - "execution_count": 4, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -84,25 +115,25 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 18, "id": "b95b8dff-dc50-4505-91a8-562fa6f2d5b9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "count 1426.000000\n", - "mean 0.528424\n", - "std 0.284260\n", + "count 2377.000000\n", + "mean 0.279072\n", + "std 0.319965\n", "min 0.000000\n", - "25% 0.391550\n", - "50% 0.486150\n", - "75% 0.797150\n", + "25% 0.000000\n", + "50% 0.266099\n", + "75% 0.452338\n", "max 1.000000\n", "Name: score, dtype: float64" ] }, - "execution_count": 5, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -113,25 +144,25 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 19, "id": "9b1f6e3d-3912-4b97-ba50-0ff3894b17c5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "count 1426.000000\n", - "mean 0.532307\n", - "std 0.224664\n", - "min 0.170800\n", - "25% 0.373025\n", - "50% 0.452750\n", - "75% 0.589400\n", + "count 1460.000000\n", + "mean 0.445758\n", + "std 0.256881\n", + "min 0.000000\n", + "25% 0.272312\n", + "50% 0.360000\n", + "75% 0.527648\n", "max 1.000000\n", "Name: score, dtype: float64" ] }, - "execution_count": 6, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -142,13 +173,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 20, "id": "4a520159-6ef0-4def-b1fb-d96c3412d8ca", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -163,13 +194,13 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 21, "id": "cdce096a-6081-48d6-861a-a0b39cda5544", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAAGdCAYAAAAMm0nCAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIolJREFUeJzt3Q2QVeV9P/AfC8trBMQMIAkitTGiYkgkEnxJE+XFQK0YpokjtaSlkEZJi8xoJEHCm0EpIVaDUlMVnWpMbKM1SBGCVWpEUQytokFbTbS1QFNFFMqysPc/z/nP3bC4GjX3sjy7n8/M8e4599zD4beHe78+L+e2K5VKpQAAyEhNS58AAMB7JcAAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZKdDtFINDQ3xyiuvxGGHHRbt2rVr6dMBAN6FdH/dN954I/r16xc1NTVtL8Ck8NK/f/+WPg0A4H14+eWX48Mf/nDbCzCp5aVcgO7du1fsuPX19bFq1aoYNWpU1NbWVuy4qHVLcU2rc2vies6/zjt27CgaIMqf420uwJS7jVJ4qXSA6dq1a3FMAaa61PrgUGd1bk1cz62nzr9p+IdBvABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAstOhpU8gVyfOvj/q9r3zV32/H7+4amzFjwkArY0WGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAND6A8zatWvjnHPOiX79+kW7du3innvuafJ8qVSKWbNmxZFHHhldunSJESNGxPPPP99kn1dffTUmTJgQ3bt3j549e8akSZPizTffbLLPv/3bv8UZZ5wRnTt3jv79+8fChQvf798RAGjrAWbnzp3xsY99LJYsWdLs8yloXHvttbF06dJ47LHHolu3bjF69OjYvXt34z4pvGzatClWr14dy5cvL0LRlClTGp/fsWNHjBo1KgYMGBAbNmyIv/qrv4rZs2fHjTfe+H7/ngBAK9Lhvb7gc5/7XLE0J7W+XHPNNTFz5sw499xzi2233XZb9OnTp2ipOf/88+PZZ5+NlStXxuOPPx5Dhw4t9rnuuutizJgxsWjRoqJl5/bbb489e/bEzTffHB07dowTTjghNm7cGIsXL24SdACAtuk9B5h38uKLL8aWLVuKbqOyHj16xLBhw2LdunVFgEmPqduoHF6StH9NTU3RYnPeeecV+3z6058uwktZasW5+uqr47XXXovDDz/8LX92XV1dsezfipPU19cXS6WUj9WpplSxYzZ3fH5dCzWpLnU+ONRZnVuT+iq+P7/bY1Y0wKTwkqQWl/2l9fJz6bF3795NT6JDh+jVq1eTfQYOHPiWY5Sfay7ALFiwIObMmfOW7atWrYquXbtGpc0b2hDVsGLFiqocN2epqxF1bi1cz+rcmqyuwvvzrl27Dn6AaUkzZsyI6dOnN2mBSYN/01iaNFi4kskw/cKueKIm6hraRaU9PXt0xY+Zq3KtR44cGbW1tS19Oq2WOqtza+J6zr/O5R6Ugxpg+vbtWzxu3bq1mIVUltaHDBnSuM+2bduavG7v3r3FzKTy69Njes3+yuvlfQ7UqVOnYjlQKmw1PvxSeKnbV/kA44O6+ZqoS/Wp88GhzurcmtRW4f353R6voveBSd0+KWCsWbOmSZJKY1uGDx9erKfH7du3F7OLyh544IFoaGgoxsqU90kzk/bvB0tJ76Mf/Wiz3UcAQNvyngNMul9LmhGUlvLA3fTzSy+9VNwXZtq0aTF//vy4995746mnnoo//uM/LmYWjRs3rth/0KBBcfbZZ8fkyZNj/fr18dOf/jSmTp1aDPBN+yUXXHBBMYA33R8mTbf+wQ9+EH/913/dpIsIAGi73nMX0hNPPBGf/exnG9fLoWLixImxbNmyuOyyy4p7xaTpzqml5fTTTy+mTacb0pWladIptJx11lnF7KPx48cX947Zf+ZSGnx78cUXx8knnxwf/OAHi5vjmUINALyvAPOZz3ymuN/L20mtMHPnzi2Wt5NmHN1xxx3v+OecdNJJ8S//8i9+SwDAW/guJAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkp+IBZt++fXHFFVfEwIEDo0uXLnHMMcfEvHnzolQqNe6Tfp41a1YceeSRxT4jRoyI559/vslxXn311ZgwYUJ07949evbsGZMmTYo333yz0qcLAGSo4gHm6quvjhtuuCG++93vxrPPPlusL1y4MK677rrGfdL6tddeG0uXLo3HHnssunXrFqNHj47du3c37pPCy6ZNm2L16tWxfPnyWLt2bUyZMqXSpwsAZKhDpQ/4yCOPxLnnnhtjx44t1o8++uj4/ve/H+vXr29sfbnmmmti5syZxX7JbbfdFn369Il77rknzj///CL4rFy5Mh5//PEYOnRosU8KQGPGjIlFixZFv379Kn3aAEBbDjCnnnpq3HjjjfHcc8/FscceG//6r/8aDz/8cCxevLh4/sUXX4wtW7YU3UZlPXr0iGHDhsW6deuKAJMeU7dRObwkaf+ampqixea88857y59bV1dXLGU7duwoHuvr64ulUsrH6lTz6y6xSqrkueauXAs1UefWwPWszq1JfRXfn9/tMSseYC6//PIiPBx33HHRvn37YkzMlVdeWXQJJSm8JKnFZX9pvfxceuzdu3fTE+3QIXr16tW4z4EWLFgQc+bMecv2VatWRdeuXaPS5g1tiGpYsWJFVY6bs9SNiDq3Fq5ndW5NVlfh/XnXrl0tE2B++MMfxu233x533HFHnHDCCbFx48aYNm1a0e0zceLEqJYZM2bE9OnTG9dTiOrfv3+MGjWqGAhcyWSYfmFXPFETdQ3totKenj264sfMVbnWI0eOjNra2pY+nVZLndW5NXE951/ncg/KQQ8wl156adEKk7qCksGDB8cvf/nLooUkBZi+ffsW27du3VrMQipL60OGDCl+Tvts27atyXH37t1bzEwqv/5AnTp1KpYDpcJW48MvhZe6fZUPMD6om6+JulSfOh8c6qzOrUltFd6f3+3xaqrR9JPGquwvdSU1NPz/Lpc0vTqFkDVr1jRJW2lsy/Dhw4v19Lh9+/bYsGFD4z4PPPBAcYw0VgYAaNsq3gJzzjnnFGNejjrqqKIL6Wc/+1kxgPdP//RPi+fbtWtXdCnNnz8/PvKRjxSBJt03JnUxjRs3rthn0KBBcfbZZ8fkyZOLqdapqWrq1KlFq44ZSABAxQNMmu6cAslFF11UdAOlwPHlL3+5uHFd2WWXXRY7d+4s7uuSWlpOP/30Ytp0586dG/dJ42hSaDnrrLOKFp3x48cX944BAKh4gDnssMOK+7yk5e2kVpi5c+cWy9tJM47SQGAAgAP5LiQAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMhOVQLMf/3Xf8Uf/dEfxRFHHBFdunSJwYMHxxNPPNH4fKlUilmzZsWRRx5ZPD9ixIh4/vnnmxzj1VdfjQkTJkT37t2jZ8+eMWnSpHjzzTercboAQFsPMK+99lqcdtppUVtbG//0T/8UzzzzTHz729+Oww8/vHGfhQsXxrXXXhtLly6Nxx57LLp16xajR4+O3bt3N+6TwsumTZti9erVsXz58li7dm1MmTKl0qcLAGSoQ6UPePXVV0f//v3jlltuadw2cODAJq0v11xzTcycOTPOPffcYtttt90Wffr0iXvuuSfOP//8ePbZZ2PlypXx+OOPx9ChQ4t9rrvuuhgzZkwsWrQo+vXrV+nTBgAyUvEAc++99xatKX/4h38YDz30UHzoQx+Kiy66KCZPnlw8/+KLL8aWLVuKbqOyHj16xLBhw2LdunVFgEmPqduoHF6StH9NTU3RYnPeeee95c+tq6srlrIdO3YUj/X19cVSKeVjdaopVeyYzR2fX9dCTapLnQ8OdVbn1qS+iu/P7/aYFQ8wL7zwQtxwww0xffr0+PrXv160ovzFX/xFdOzYMSZOnFiElyS1uOwvrZefS4+9e/dueqIdOkSvXr0a9znQggULYs6cOW/ZvmrVqujatWtU2ryhDVENK1asqMpxc5a6EVHn1sL1rM6tyeoqvD/v2rWrZQJMQ0ND0XLyrW99q1j/+Mc/Hk8//XQx3iUFmGqZMWNGEZr2b4FJXVmjRo0qBgJXMhmmX9gVT9REXUO7qLSnZ4+u+DFzVa71yJEjizFVqHPOXM/q3JrUV/H9udyDctADTJpZdPzxxzfZNmjQoPiHf/iH4ue+ffsWj1u3bi32LUvrQ4YMadxn27ZtTY6xd+/eYmZS+fUH6tSpU7EcKBW2Gh9+KbzU7at8gPFB3XxN1KX61PngUGd1bk1qq/D+/G6PV/FZSGkG0ubNm5tse+6552LAgAGNA3pTCFmzZk2TtJXGtgwfPrxYT4/bt2+PDRs2NO7zwAMPFK07aawMANC2VbwF5pJLLolTTz216EL6whe+EOvXr48bb7yxWJJ27drFtGnTYv78+fGRj3ykCDRXXHFFMbNo3LhxjS02Z599djHwN3U9paaqqVOnFgN8zUACACoeYD75yU/G3XffXYxJmTt3bhFQ0rTpdF+Xsssuuyx27txZ3NcltbScfvrpxbTpzp07N+5z++23F6HlrLPOKmYfjR8/vrh3DABAxQNM8vu///vF8nZSK0wKN2l5O2nG0R133OE3BAC8he9CAgCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2qh5grrrqqmjXrl1Mmzatcdvu3bvj4osvjiOOOCI+8IEPxPjx42Pr1q1NXvfSSy/F2LFjo2vXrtG7d++49NJLY+/evdU+XQCgrQeYxx9/PP7mb/4mTjrppCbbL7nkkvjxj38cd911Vzz00EPxyiuvxOc///nG5/ft21eElz179sQjjzwSt956ayxbtixmzZpVzdMFANp6gHnzzTdjwoQJ8b3vfS8OP/zwxu2vv/563HTTTbF48eI488wz4+STT45bbrmlCCqPPvposc+qVavimWeeib/7u7+LIUOGxOc+97mYN29eLFmypAg1AEDb1qFaB05dRKkVZcSIETF//vzG7Rs2bIj6+vpie9lxxx0XRx11VKxbty4+9alPFY+DBw+OPn36NO4zevTo+MpXvhKbNm2Kj3/842/58+rq6oqlbMeOHcVj+rPSUinlY3WqKVXsmM0dn1/XQk2qS50PDnVW59akvorvz+/2mFUJMHfeeWc8+eSTRRfSgbZs2RIdO3aMnj17Ntmewkp6rrzP/uGl/Hz5ueYsWLAg5syZ85btqTUnjaOptHlDG6IaVqxYUZXj5mz16tUtfQptgjqrc2vies63zrt27WqZAPPyyy/HX/7lXxZ/qc6dO8fBMmPGjJg+fXqTFpj+/fvHqFGjonv37hVNhunvdsUTNVHX0C4q7enZoyt+zFyVaz1y5Miora1t6dNptdRZnVsT13P+dS73oBz0AJO6iLZt2xaf+MQnmgzKXbt2bXz3u9+N+++/vxjHsn379iatMGkWUt++fYuf0+P69eubHLc8S6m8z4E6depULAdKha3Gh18KL3X7Kh9gfFA3XxN1qT51PjjUWZ1bk9oqvD+/2+NVfBDvWWedFU899VRs3LixcRk6dGgxoLf8czq5NWvWNL5m8+bNxbTp4cOHF+vpMR0jBaGylPRSS8rxxx9f6VMGADJT8RaYww47LE488cQm27p161bc86W8fdKkSUV3T69evYpQ8tWvfrUILWkAb5K6fVJQufDCC2PhwoXFuJeZM2cWA4Oba2UBANqWqs1Ceiff+c53oqampriBXZo5lGYYXX/99Y3Pt2/fPpYvX17MOkrBJgWgiRMnxty5c1vidAGAthhgHnzwwSbraXBvuqdLWt7OgAEDzMgBAJrlu5AAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7AgwAkB0BBgDIjgADAGRHgAEAsiPAAADZEWAAgOwIMABAdgQYACA7FQ8wCxYsiE9+8pNx2GGHRe/evWPcuHGxefPmJvvs3r07Lr744jjiiCPiAx/4QIwfPz62bt3aZJ+XXnopxo4dG127di2Oc+mll8bevXsrfboAQIYqHmAeeuihIpw8+uijsXr16qivr49Ro0bFzp07G/e55JJL4sc//nHcddddxf6vvPJKfP7zn298ft++fUV42bNnTzzyyCNx6623xrJly2LWrFmVPl0AIEMdKn3AlStXNllPwSO1oGzYsCE+/elPx+uvvx433XRT3HHHHXHmmWcW+9xyyy0xaNCgIvR86lOfilWrVsUzzzwTP/nJT6JPnz4xZMiQmDdvXnzta1+L2bNnR8eOHSt92gBARqo+BiYFlqRXr17FYwoyqVVmxIgRjfscd9xxcdRRR8W6deuK9fQ4ePDgIryUjR49Onbs2BGbNm2q9ikDAG2tBWZ/DQ0NMW3atDjttNPixBNPLLZt2bKlaEHp2bNnk31TWEnPlffZP7yUny8/15y6urpiKUthJ0lhKS2VUj5Wp5pSxY7Z3PH5dS3UpLrU+eBQZ3VuTeqr+P78bo9Z1QCTxsI8/fTT8fDDD0e1pcHDc+bMecv21B2VBgJX2ryhDVENK1asqMpxc5bGUqHOrYXrWZ1bk9VVeH/etWtXywaYqVOnxvLly2Pt2rXx4Q9/uHF73759i8G527dvb9IKk2YhpefK+6xfv77J8cqzlMr7HGjGjBkxffr0Ji0w/fv3LwYQd+/evaLJMP3CrniiJuoa2kWlPT17dMWPmatyrUeOHBm1tbUtfTqtljqrc2vies6/zuUelIMeYEqlUnz1q1+Nu+++Ox588MEYOHBgk+dPPvnk4i+7Zs2aYvp0kqZZp2nTw4cPL9bT45VXXhnbtm0rBgAnqVApiBx//PHN/rmdOnUqlgOlP6saH34pvNTtq3yA8UHdfE0qXZejL78vquUXV42NHFXr3wrq3BJcz/nW+d0er0M1uo3SDKN//Md/LO4FUx6z0qNHj+jSpUvxOGnSpKK1JA3sTaEkBZ4UWtIMpCS1mqSgcuGFF8bChQuLY8ycObM4dnMhBQBoWyoeYG644Ybi8TOf+UyT7Wmq9Je+9KXi5+985ztRU1NTtMCkgbdphtH111/fuG/79u2L7qevfOUrRbDp1q1bTJw4MebOnVvp0wUAMlSVLqTfpHPnzrFkyZJieTsDBgwwoJUsVat7KteuKYBq8F1IAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7AgwAEB2BBgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7HRo6RMA3p2jL7+vKqXq1L4UC0/xWwDyogUGAMiOAAMAZEcXElA4cfb9UbevXUWr8YurxqoutMLu506HQNezFhgAIDsCDACQHV1IQHYzpxLdU9C2aYEBALIjwAAA2RFgAIDsCDAAQHYEGAAgO2YhAVmq1gwns5sgDwIMALTiWw60VrqQAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABALLjPjAAB+F+HJ3al2LhKUoNlSLAANDqnDj7/qjb167ix3Wn5kOHLiQAIDtaYADgXXLL/0OHFhgAIDsCDACQHQEGAMiOAAMAZEeAAQCyI8AAANkRYACA7LgPDG32jpoA5EsLDACQHS0wALSau9r60sy2QwsMAJCdQzrALFmyJI4++ujo3LlzDBs2LNavX9/SpwQAHAIO2QDzgx/8IKZPnx7f/OY348knn4yPfexjMXr06Ni2bVtLnxoA0MIO2TEwixcvjsmTJ8ef/MmfFOtLly6N++67L26++ea4/PLLW/r0ANoE377MoeqQDDB79uyJDRs2xIwZMxq31dTUxIgRI2LdunXNvqaurq5Yyl5//fXi8dVXX436+vqKnVs61q5du6JDfU3sa6j81N7//d//rfgxc1XtWvP/dWgoxa5dDep8kOqc/o3X1tZmc/l12LszcuJ6zv96fuONN4rHUqn0zucQh6Bf/epXsW/fvujTp0+T7Wn95z//ebOvWbBgQcyZM+ct2wcOHBg5+eC3W/oMaIsuaOkTaCPUWZ1bkwuqfPwUZHr06JFXgHk/UmtNGjNT1tDQULS+HHHEEdGuXeX+733Hjh3Rv3//ePnll6N79+4VOy5q3VJc0+rcmrie869zanlJ4aVfv37vuN8hGWA++MEPRvv27WPr1q1Ntqf1vn37NvuaTp06Fcv+evbsWbVzTL8wAebgUGt1bk1cz+rcmnSv0mfhO7W8HNKzkDp27Bgnn3xyrFmzpkmLSlofPnx4i54bANDyDskWmCR1B02cODGGDh0ap5xySlxzzTWxc+fOxllJAEDbdcgGmC9+8YvxP//zPzFr1qzYsmVLDBkyJFauXPmWgb0HW+qmSvemObC7CrXOlWtanVsT13PbqXO70m+apwQAcIg5JMfAAAC8EwEGAMiOAAMAZEeAAQCyI8A0Y8mSJXH00UdH586dY9iwYbF+/fp3LOJdd90Vxx13XLH/4MGDY8WKFdX6fbXpWn/ve9+LM844Iw4//PBiSd+N9Zt+N7z3Ou/vzjvvLO5kPW7cOKWs8PWcbN++PS6++OI48sgji9kcxx57rPePKtQ53Ybjox/9aHTp0qW4e+wll1wSu3fvdk2/g7Vr18Y555xT3A03vQfcc8898Zs8+OCD8YlPfKK4ln/3d383li1bFlWVZiHxa3feeWepY8eOpZtvvrm0adOm0uTJk0s9e/Ysbd26tdky/fSnPy21b9++tHDhwtIzzzxTmjlzZqm2trb01FNPKWuFa33BBReUlixZUvrZz35WevbZZ0tf+tKXSj169Cj953/+p1pXsM5lL774YulDH/pQ6Ywzziide+65alzh67murq40dOjQ0pgxY0oPP/xwUe8HH3ywtHHjRrWuYJ1vv/32UqdOnYrHVOP777+/dOSRR5YuueQSdX4HK1asKH3jG98o/ehHP0ozlUt33333O+1eeuGFF0pdu3YtTZ8+vfgsvO6664rPxpUrV5aqRYA5wCmnnFK6+OKLG9f37dtX6tevX2nBggXNFvALX/hCaezYsU22DRs2rPTlL3+5Gr+vNl3rA+3du7d02GGHlW699dYqnmXbrHOq7amnnlr627/929LEiRMFmCrU+YYbbij9zu/8TmnPnj3v7Rfaxr3XOqd9zzzzzCbb0ofsaaedVvVzbS3iXQSYyy67rHTCCSc02fbFL36xNHr06Kqdly6k/ezZsyc2bNhQdE2U1dTUFOvr1q1rtgUrbd9//2T06NFvuz/vv9YH2rVrV9TX10evXr2UtcJ1njt3bvTu3TsmTZqktlWq87333lt8NUrqQko36DzxxBPjW9/6Vuzbt0/NK1jnU089tXhNuZvphRdeKLrpxowZo84V1BKfhYfsnXhbwq9+9avizePAu/2m9Z///OfNvibdJbi5/dN2KlvrA33ta18r+mcP/EfDb1fnhx9+OG666abYuHGjUlaxzumD9IEHHogJEyYUH6j//u//HhdddFERytMdTqlMnS+44ILidaeffnrxLcd79+6NP//zP4+vf/3rSlxBb/dZmL61+v/+7/+K8UeVpgWGLF111VXFANO77767GMhHZaSvsL/wwguLAdPpW+GpnvQFtamV68Ybbyy+vDZ9fco3vvGNWLp0qbJXUBpYmlq2rr/++njyySfjRz/6Udx3330xb948dc6cFpj9pDfs9u3bx9atW5sUKa337du32QKm7e9lf95/rcsWLVpUBJif/OQncdJJJylpBev8H//xH/GLX/yimH2w/wdt0qFDh9i8eXMcc8wxav5b1jlJM49qa2uL15UNGjSo+D/Z1FXSsWNHda5Ana+44ooilP/Zn/1ZsZ5miqYvBp4yZUoRGFMXFL+9t/ss7N69e1VaXxK/uf2kN4z0f0Jr1qxp8uad1lNfdXPS9v33T1avXv22+/P+a50sXLiw+D+n9MWe6ZvKqWyd0+0AnnrqqaL7qLz8wR/8QXz2s58tfk5TUPnt65ycdtppRbdROSAmzz33XBFshJfKXM/lsXIHhpRyaPRVgJXTIp+FVRsenPEUvTTlbtmyZcVUsClTphRT9LZs2VI8f+GFF5Yuv/zyJtOoO3ToUFq0aFExtfeb3/ymadRVqvVVV11VTJ/8+7//+9J///d/Ny5vvPFGZS+CNl7nA5mFVJ06v/TSS8UsuqlTp5Y2b95cWr58eal3796l+fPn/5a/8dbtvdY5vSenOn//+98vpvquWrWqdMwxxxQzSHl76X013bIiLSkqLF68uPj5l7/8ZfF8qnGq9YHTqC+99NLiszDd8sI06haQ5q8fddRRxYdlmrL36KOPNj73e7/3e8Ub+v5++MMflo499thi/zSN7L777muBs279tR4wYEDxD+nAJb1BUbk6H0iAqc71nDzyyCPFbRfSB3KaUn3llVcWU9ipXJ3r6+tLs2fPLkJL586dS/379y9ddNFFpddee02Z38E///M/N/t+W65teky1PvA1Q4YMKX4v6Xq+5ZZbStXULv2neu07AACVZwwMAJAdAQYAyI4AAwBkR4ABALIjwAAA2RFgAIDsCDAAQHYEGAAgOwIMAJAdAQYAyI4AAwBkR4ABACI3/w9Gn8pqDTyuIAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -184,13 +215,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 22, "id": "c101439b-f429-4529-9831-d182922d95d6", "metadata": {}, "outputs": [ { "data": { - "image/png": "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josFArRRX72gH0SjBwUACEdAAQDCEVAAgHAEFAAgHAEFAAhHQAEAwhFQAIBwnAellc8NUF1VSFMHp9R/4qOpdm3FN6kNALRZelAAgHAEFAAgHId4AIKfNv79KccWbdsQlR4UACAcPSgAwbmoH22RHhQAIBwBBQAIR0ABAMIRUACAcAQUACAcAQUACMc0Y6BNMWUXyoMeFAAgnJIGlGnTpqU+ffqkDh06pCFDhqTnnnuulM0BANp6QPntb3+bJkyYkK655pr04osvpgMPPDDV1NSkpUuXlqpJAEBbDyg33XRTOvvss9NZZ52V9t1333THHXekjh07prvvvrtUTQIA2vIg2dWrV6eFCxemyy67rPGxysrKNGLEiLRgwYKvrV9bW5svDT7//PP89rPPPkt1dXWt3r52a77c/NfWF9LKlfWpXV1lWltf0artovWpV/lRs/KjZuVbs08//TS1b9++1ba7YsWK/LZQKGy6DakEPvnkk7R27dq08847N3k8u//GG298bf3JkyenSZMmfe3xvn37pohOLXUDaBH1Kj9qVn7UrPycWsRtZ0Glc+fO5T/NOOtpycarNKivr897T7p165YqKmL1Uixfvjz17t07ffjhh6lTp06lbg6boF7lR83Kj5qVn+VF2pdlPSdZOOnVq9cm1y1JQNlpp51SVVVVWrJkSZPHs/s9evT42vrV1dX58lVdunRJkWUFFVDKh3qVHzUrP2pWfjoVYV+2qZ6Tkg6S3WabbdLAgQPTvHnzmvSKZPeHDh1aiiYBAIGU7BBPdsjmjDPOSIMGDUqDBw9Ot9xyS/ryyy/zWT0AQNtWsoBy8sknp3//+9/p6quvTosXL04DBgxIs2fP/trA2XKTHYrKzu2y7iEpYlKv8qNm5UfNyk91gH1ZRaE5c30AALYg1+IBAMIRUACAcAQUACAcAQUACEdAaaFp06alPn36pA4dOqQhQ4ak5557boPr/vKXv0yHHXZY2nHHHfMlu9bQxtan9DX7qgceeCA/U/EJJ5ygNMFrtmzZsnTeeeelnj175rMO9tprrzRr1qwt1l5aXrPs1BJ777132nbbbfMzlo4fPz6tWrXKW7kFPPXUU2n06NH52Vyz77hHHnlkk6954okn0ne/+93887Xnnnum6dOnF7+h2SwemueBBx4obLPNNoW777678OqrrxbOPvvsQpcuXQpLlixZ7/qnnnpqYdq0aYW//e1vhddff71w5plnFjp37lz46KOPvOVBa9bgvffeK3z7298uHHbYYYXjjz9evQLXrLa2tjBo0KDCqFGjCvPnz89r98QTTxReeukldQtas3vvvbdQXV2d32b1evTRRws9e/YsjB8/Xs22gFmzZhWuuOKKwkMPPZTN4i08/PDDG13/3XffLXTs2LEwYcKEwmuvvVa49dZbC1VVVYXZs2cXtZ0CSgsMHjy4cN555zXeX7t2baFXr16FyZMnN+v1a9asKeywww6FX//61y2vFFusZlmdDjnkkMKvfvWrwhlnnCGgBK/Z7bffXth9990Lq1ev3oKt5JvULFt3+PDhTR7Ldn6HHnqoN3YLS80IKBdffHFhv/32a/LYySefXKipqSlq2xziaabVq1enhQsX5odpGlRWVub3FyxY0KxtrFy5MtXV1aWuXbtuXncXW6Rm1157berevXsaO3asd7wMavaHP/whv0RGdognO9Fj//790w033JBfMZ2YNTvkkEPy1zQcBnr33XfzQ3KjRo1SsoAWLFjQpL6ZmpqaZu/7NldZXM04gk8++ST/wlv3TLfZ/TfeeKNZ27jkkkvyY37rFpo4NZs/f36666670ksvvaQsZVKzbOf2+OOPp9NOOy3fyb399tvp3HPPzf8YyM6ESbyanXrqqfnrhg0bll/dds2aNeknP/lJuvzyy5UroMWLF6+3vtkVj//73//m44iKQQ/KFjJlypR80OXDDz+cDyIjnuwS4Keffno+uDm74jblIbvQaNbjdeedd+YXIc0uo3HFFVekO+64o9RNYyMDLrNerttuuy29+OKL6aGHHkozZ85M1113nfeMRnpQminbYVVVVaUlS5Y0eTy736NHj42+9sYbb8wDymOPPZYOOOCA5v5ItnDN3nnnnfT+++/no9u/uvPLtGvXLr355ptpjz32UJdgn7Ns5k779u3z1zXo169f/ldfdvghu3o6sWp21VVX5X8M/PjHP87v77///vnFYseNG5eHy+wQEXFkdVxffTt16lS03pOM/wuaKfuSy/46mzdvXpOdV3Y/O/69IVOnTs3/KsguhJhduZm4Ndtnn33Syy+/nB/eaViOO+64dMQRR+T/zqZCEqtmmUMPPTQ/rNMQJjP/+Mc/8uAinMSsWTYeb90Q0hAwXR4unqFDhzapb2bu3Lkb3fe1iqIOwd0Kp9JlU+OmT5+eT7UaN25cPpVu8eLF+fOnn3564dJLL21cf8qUKfnUu9/97neFRYsWNS4rVqwo4W/RtrS0Zusyiyd+zT744IN8dtz5559fePPNNwszZswodO/evXD99deXoPVtU0trds011+Q1u//++/MprHPmzCnssccehR/84Acl/C3ajhUrVuSnv8iWLAbcdNNN+b//+c9/5s9ntcpqtu4044suuig/ZUZ2+gzTjAPK5n/vuuuuefDIptY988wzjc99//vfz3doDXbbbbe8+Osu2YeTmDVbl4BSHjV7+umnC0OGDMl3ktmU45/97Gf5dHFi1qyurq4wceLEPJR06NCh0Lt378K5555b+M9//qNkW8Cf//zn9e6bGmqU3WY1W/c1AwYMyOubfcbuueeeorezIvtPcftoAABaxhgUACAcAQUACEdAAQDCEVAAgHAEFAAgHAEFAAhHQAEAwhFQAIBwBBQAIBwBBQAIR0ABAMIRUACAFM3/A299nSLZhZEWAAAAAElFTkSuQmCC", + "image/png": "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xYrSwIEDS7NmzWr3ba8my5YtK33rW98q3XvvvXEWb+m+++57z/V/97vflXr27FmaPXt2+h68+eab0/fi8uXLC93OLhdQRo0aVZoxY0bL/d27d5cGDRpUmj9//n7X/7M/+7PSxIkTWz02evTo0l//9V8Xvq1dqc5727VrV+mII44o/fCHPyxwK7tmnWNtP/3pT5e+//3vly666CIBpYA633rrraWPfOQjpZ07d7btF9rFtbXOcd2zzjqr1WPxS/S0004rfFs7i/A+Asqll15a+vjHP97qsS996UulxsbGQretSx3i2blzZ1i3bl06fFBWW1ub7q9du3a/r4mPv3v9qLGx8YDrc2h13tuOHTtCc3Nz6Nu3r5JWcH+Orr322tC/f/8wdepUtS2ozv/6r/+aLtsRD/HEk08OHz48fOc730lXcadydf70pz+dXlM+DPS73/0uHUabMGGCMldQR30PVsXVjCvllVdeSR8Qe5+tNt7/zW9+s9/XxLPc7m/9+DiVq/PeLrvssnR8dO9/FHywOj/yyCPhjjvuCOvXr1fKAuscvygfeuihcMEFF6QvzE2bNoWvf/3rKXTHs3NSmTpPmTIlve70009PV8ndtWtX+OpXvxquuOIKJa6gA30Pxise/9///V8a/1OELtWDQnW44YYb0gDO++67Lw2UozLiJc4vvPDCNCA5XlGc4sSLn8Zeqttvvz1dGDVe2uNb3/pWuO2225S9guLAzdgzdcstt4Rf/epX4d577w1Lly4N1113nTp3Al2qByV+KNfV1YUtW7a0ejzeHzBgwH5fEx9vy/ocWp3LbrzxxhRQHnzwwXDyyScrZwX352effTY8//zzafT+u79Io27duoWNGzeG448/Xs0/YJ2jOHOnvr4+va5s2LBh6X+i8VBGvKI7H7zOV111VQrdf/VXf5Xux1mW8aKz06dPT4EwHiLigzvQ92CvXr0K6z2JutRvL34oxP/NrF69utUHdLwfjxfvT3z83etHq1atOuD6HFqdowULFqT/+cSLRsarXFPZ/TlOlX/yySfT4Z3ycu6554Yzzzwz/T1O0eSD1zk67bTT0mGdcgCM/uu//isFF+GkMvtzeaza3iGkHApdZq5yOux7sNQFp7HFaWmLFi1K06WmT5+eprFt3rw5PX/hhReWLr/88lbTjLt161a68cYb0/TXa665xjTjAup8ww03pOmF//Iv/1J6+eWXW5Y33nij8jtBF67z3sziKabOL7zwQpqFNnPmzNLGjRtLS5YsKfXv3790/fXXf8DfeOfW1jrHz+NY5x//+MdpKuzKlStLxx9/fJp9yYHFz9V4Soe4xBhw0003pb///ve/T8/HGsda7z3N+JJLLknfg/GUEKYZFyTO4T7mmGPSF2Kc1vbYY4+1PPe5z30ufWi/2z//8z+XTjjhhLR+nGq1dOnSojaty9b52GOPTf9Q9l7iBxCVq/PeBJRi9ufo0UcfTackiF+4ccrxt7/97TTFm8rVubm5uTR37twUSrp3714aPHhw6etf/3rpf//3f5X5Pfz7v//7fj9vy7WNt7HWe79mxIgR6fcS9+c777yzVLSa+EexfTQAAG3TpcagAADVQUABALIjoAAA2RFQAIDsCCgAQHYEFAAgOwIKAJAdAQUAyI6AAgBkR0ABALIjoAAA2RFQAICQm/8HkUwgfb3ZZsAAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -205,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 23, "id": "d5fda75a-93fc-4d0d-827b-ef57c9cc2993", "metadata": {}, "outputs": [ @@ -213,7 +244,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "218\n" + "52\n" ] }, { @@ -237,9 +268,9 @@ " \n", " \n", " \n", - " py_namespace\n", - " py_member_of\n", - " py_name\n", + " python_namespace\n", + " python_member_of\n", + " python_name\n", " go_namespace\n", " go_member_of\n", " go_name\n", @@ -251,67 +282,67 @@ " \n", " \n", " \n", - " 1399\n", - " flows.llm_flows\n", + " 430\n", + " agents\n", " NaN\n", - " merge_parallel_function_response_events\n", - " internal.utils\n", + " from_config\n", + " internal.agent.parentmap\n", " NaN\n", - " FunctionResponses\n", + " FromContext\n", " function\n", - " 0.4991\n", + " 0.498214\n", " False\n", " high\n", " \n", " \n", - " 263\n", - " plugins\n", - " BasePlugin\n", - " on_model_error_callback\n", - " internal.plugininternal\n", - " PluginManager\n", - " RunOnModelErrorCallback\n", + " 648\n", + " sessions\n", + " InMemorySessionService\n", + " get_session\n", + " memory\n", + " InMemoryService\n", + " AddSession\n", " method\n", - " 0.4989\n", + " 0.497182\n", " False\n", " high\n", " \n", " \n", - " 1213\n", - " tools.openapi_tool.auth\n", + " 442\n", + " utils\n", " NaN\n", - " dict_to_auth_scheme\n", - " internal.typeutil\n", + " is_gemini_2_or_above\n", + " internal.llminternal.googlellm\n", " NaN\n", - " ConvertToWithJSONSchema\n", + " IsGemini25OrLower\n", " function\n", - " 0.4989\n", + " 0.495882\n", " False\n", " high\n", " \n", " \n", - " 1288\n", - " cli.plugins\n", - " ReplayPlugin\n", - " __init__\n", - " plugin\n", - " Plugin\n", - " New\n", - " constructor\n", - " 0.4989\n", + " 357\n", + " agents\n", + " BaseAgent\n", + " root_agent\n", + " agent\n", + " Loader\n", + " RootAgent\n", + " method\n", + " 0.495000\n", " False\n", " high\n", " \n", " \n", - " 219\n", - " plugins\n", - " HybridContentParser\n", - " parse\n", - " cmd.launcher.console\n", - " ConsoleLauncher\n", - " Parse\n", - " method\n", - " 0.4983\n", + " 764\n", + " evaluation\n", + " InvocationEvent\n", + " InvocationEvent\n", + " internal.context\n", + " InvocationContext\n", + " NewInvocationContext\n", + " constructor\n", + " 0.493333\n", " False\n", " high\n", " \n", @@ -320,36 +351,29 @@ "" ], "text/plain": [ - " py_namespace py_member_of \\\n", - "1399 flows.llm_flows NaN \n", - "263 plugins BasePlugin \n", - "1213 tools.openapi_tool.auth NaN \n", - "1288 cli.plugins ReplayPlugin \n", - "219 plugins HybridContentParser \n", - "\n", - " py_name go_namespace \\\n", - "1399 merge_parallel_function_response_events internal.utils \n", - "263 on_model_error_callback internal.plugininternal \n", - "1213 dict_to_auth_scheme internal.typeutil \n", - "1288 __init__ plugin \n", - "219 parse cmd.launcher.console \n", + " python_namespace python_member_of python_name \\\n", + "430 agents NaN from_config \n", + "648 sessions InMemorySessionService get_session \n", + "442 utils NaN is_gemini_2_or_above \n", + "357 agents BaseAgent root_agent \n", + "764 evaluation InvocationEvent InvocationEvent \n", "\n", - " go_member_of go_name type score match \\\n", - "1399 NaN FunctionResponses function 0.4991 False \n", - "263 PluginManager RunOnModelErrorCallback method 0.4989 False \n", - "1213 NaN ConvertToWithJSONSchema function 0.4989 False \n", - "1288 Plugin New constructor 0.4989 False \n", - "219 ConsoleLauncher Parse method 0.4983 False \n", + " go_namespace go_member_of go_name \\\n", + "430 internal.agent.parentmap NaN FromContext \n", + "648 memory InMemoryService AddSession \n", + "442 internal.llminternal.googlellm NaN IsGemini25OrLower \n", + "357 agent Loader RootAgent \n", + "764 internal.context InvocationContext NewInvocationContext \n", "\n", - " confidence \n", - "1399 high \n", - "263 high \n", - "1213 high \n", - "1288 high \n", - "219 high " + " type score match confidence \n", + "430 function 0.498214 False high \n", + "648 method 0.497182 False high \n", + "442 function 0.495882 False high \n", + "357 method 0.495000 False high \n", + "764 constructor 0.493333 False high " ] }, - "execution_count": 10, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -366,7 +390,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "id": "dace4311-5318-4196-a982-3dafa6e39747", "metadata": {}, "outputs": [ @@ -374,7 +398,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "252\n" + "147\n" ] }, { @@ -398,9 +422,9 @@ " \n", " \n", " \n", - " py_namespace\n", - " py_member_of\n", - " py_name\n", + " python_namespace\n", + " python_member_of\n", + " python_name\n", " java_namespace\n", " java_member_of\n", " java_name\n", @@ -412,105 +436,98 @@ " \n", " \n", " \n", - " 1\n", - " runners\n", - " Runner\n", - " rewind_async\n", - " runner\n", - " Runner\n", - " runAsync\n", - " method\n", - " 0.5978\n", - " True\n", - " low\n", + " 906\n", + " code_executors\n", + " CodeExecutionResult\n", + " CodeExecutionResult\n", + " codeexecutors\n", + " CodeExecutionUtils\n", + " CodeExecutionUtils\n", + " constructor\n", + " 0.598810\n", + " False\n", + " high\n", " \n", " \n", - " 1071\n", - " tools.computer_use\n", - " ComputerUseToolset\n", - " close\n", - " tools.mcp\n", - " McpAsyncToolset\n", - " close\n", + " 632\n", + " sessions\n", + " SqliteSessionService\n", + " get_session\n", + " sessions\n", + " BaseSessionService\n", + " createSession\n", " method\n", - " 0.5975\n", - " True\n", - " low\n", + " 0.596104\n", + " False\n", + " high\n", " \n", " \n", - " 1153\n", - " tools.application_integration_tool.clients\n", - " ConnectionsClient\n", - " get_entity_schema_and_operations\n", - " tools.applicationintegrationtoolset\n", - " ConnectionsClient\n", - " convertJsonSchemaToOpenApiSchema\n", + " 55\n", + " tools\n", + " AuthenticatedFunctionTool\n", + " run_async\n", + " tools\n", + " FunctionTool\n", + " runAsync\n", " method\n", - " 0.5970\n", - " True\n", - " low\n", + " 0.594444\n", + " False\n", + " high\n", " \n", " \n", - " 594\n", - " cli\n", - " AdkWebServer\n", - " get_session_trace\n", - " web\n", - " AdkWebServer\n", - " sessionService\n", - " method\n", - " 0.5962\n", - " True\n", - " low\n", + " 365\n", + " agents\n", + " SequentialAgentState\n", + " SequentialAgentState\n", + " agents\n", + " SequentialAgentConfig\n", + " SequentialAgentConfig\n", + " constructor\n", + " 0.593478\n", + " False\n", + " high\n", " \n", " \n", - " 1416\n", - " flows.llm_flows\n", - " BaseLlmFlow\n", - " run_async\n", - " flows.llmflows\n", - " BaseLlmFlow\n", - " run\n", - " method\n", - " 0.5959\n", - " True\n", - " low\n", + " 907\n", + " code_executors\n", + " CodeExecutionInput\n", + " CodeExecutionInput\n", + " codeexecutors\n", + " CodeExecutionUtils\n", + " CodeExecutionUtils\n", + " constructor\n", + " 0.591667\n", + " False\n", + " high\n", " \n", " \n", "\n", "" ], "text/plain": [ - " py_namespace py_member_of \\\n", - "1 runners Runner \n", - "1071 tools.computer_use ComputerUseToolset \n", - "1153 tools.application_integration_tool.clients ConnectionsClient \n", - "594 cli AdkWebServer \n", - "1416 flows.llm_flows BaseLlmFlow \n", - "\n", - " py_name java_namespace \\\n", - "1 rewind_async runner \n", - "1071 close tools.mcp \n", - "1153 get_entity_schema_and_operations tools.applicationintegrationtoolset \n", - "594 get_session_trace web \n", - "1416 run_async flows.llmflows \n", + " python_namespace python_member_of python_name \\\n", + "906 code_executors CodeExecutionResult CodeExecutionResult \n", + "632 sessions SqliteSessionService get_session \n", + "55 tools AuthenticatedFunctionTool run_async \n", + "365 agents SequentialAgentState SequentialAgentState \n", + "907 code_executors CodeExecutionInput CodeExecutionInput \n", "\n", - " java_member_of java_name type score \\\n", - "1 Runner runAsync method 0.5978 \n", - "1071 McpAsyncToolset close method 0.5975 \n", - "1153 ConnectionsClient convertJsonSchemaToOpenApiSchema method 0.5970 \n", - "594 AdkWebServer sessionService method 0.5962 \n", - "1416 BaseLlmFlow run method 0.5959 \n", + " java_namespace java_member_of java_name type \\\n", + "906 codeexecutors CodeExecutionUtils CodeExecutionUtils constructor \n", + "632 sessions BaseSessionService createSession method \n", + "55 tools FunctionTool runAsync method \n", + "365 agents SequentialAgentConfig SequentialAgentConfig constructor \n", + "907 codeexecutors CodeExecutionUtils CodeExecutionUtils constructor \n", "\n", - " match confidence \n", - "1 True low \n", - "1071 True low \n", - "1153 True low \n", - "594 True low \n", - "1416 True low " + " score match confidence \n", + "906 0.598810 False high \n", + "632 0.596104 False high \n", + "55 0.594444 False high \n", + "365 0.593478 False high \n", + "907 0.591667 False high " ] }, - "execution_count": 11, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -526,7 +543,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "id": "b71d2c93-6f89-4e1e-ad47-0cb0285d7182", "metadata": {}, "outputs": [ @@ -534,7 +551,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "102\n" + "55\n" ] }, { @@ -558,12 +575,12 @@ " \n", " \n", " \n", - " py_namespace\n", - " py_member_of\n", - " py_name\n", - " ts_namespace\n", - " ts_member_of\n", - " ts_name\n", + " python_namespace\n", + " python_member_of\n", + " python_name\n", + " typescript_namespace\n", + " typescript_member_of\n", + " typescript_name\n", " type\n", " score\n", " match\n", @@ -572,98 +589,98 @@ " \n", " \n", " \n", - " 942\n", - " tools.google_api_tool\n", - " DocsToolset\n", - " __init__\n", + " 110\n", + " tools\n", + " SetModelResponseTool\n", + " run_async\n", + " tools\n", + " GoogleSearchTool\n", + " runAsync\n", + " method\n", + " 0.554348\n", + " False\n", + " high\n", + " \n", + " \n", + " 1007\n", + " tools.pubsub\n", + " PubSubToolset\n", + " get_tools\n", " tools.mcp\n", " MCPToolset\n", - " constructor\n", - " constructor\n", - " 0.5564\n", - " True\n", - " low\n", + " getTools\n", + " method\n", + " 0.553750\n", + " False\n", + " high\n", " \n", " \n", - " 762\n", - " evaluation\n", - " NaN\n", - " get_all_tool_calls\n", - " telemetry\n", - " NaN\n", - " traceToolCall\n", - " function\n", - " 0.5551\n", - " True\n", - " low\n", + " 1008\n", + " tools.pubsub\n", + " PubSubToolset\n", + " close\n", + " tools.mcp\n", + " MCPToolset\n", + " close\n", + " method\n", + " 0.553750\n", + " False\n", + " high\n", " \n", " \n", - " 1280\n", - " cli.plugins\n", - " RecordingsPlugin\n", - " __init__\n", + " 209\n", " plugins\n", - " LoggingPlugin\n", - " constructor\n", - " constructor\n", - " 0.5527\n", - " True\n", - " low\n", - " \n", - " \n", - " 1006\n", - " tools.pubsub\n", - " PubSubToolset\n", - " __init__\n", - " tools\n", - " BaseToolset\n", - " constructor\n", - " constructor\n", - " 0.5524\n", - " True\n", - " low\n", + " BigQueryAgentAnalyticsPlugin\n", + " before_tool_callback\n", + " plugins\n", + " SecurityPlugin\n", + " beforeToolCallback\n", + " method\n", + " 0.553125\n", + " False\n", + " high\n", " \n", " \n", - " 943\n", - " tools.google_api_tool\n", - " GoogleApiTool\n", - " __init__\n", - " tools\n", - " GoogleSearchTool\n", - " constructor\n", - " constructor\n", - " 0.5521\n", - " True\n", - " low\n", + " 760\n", + " evaluation\n", + " NaN\n", + " get_all_tool_responses\n", + " events\n", + " NaN\n", + " getFunctionResponses\n", + " function\n", + " 0.552273\n", + " False\n", + " high\n", " \n", " \n", "\n", "" ], "text/plain": [ - " py_namespace py_member_of py_name \\\n", - "942 tools.google_api_tool DocsToolset __init__ \n", - "762 evaluation NaN get_all_tool_calls \n", - "1280 cli.plugins RecordingsPlugin __init__ \n", - "1006 tools.pubsub PubSubToolset __init__ \n", - "943 tools.google_api_tool GoogleApiTool __init__ \n", + " python_namespace python_member_of python_name \\\n", + "110 tools SetModelResponseTool run_async \n", + "1007 tools.pubsub PubSubToolset get_tools \n", + "1008 tools.pubsub PubSubToolset close \n", + "209 plugins BigQueryAgentAnalyticsPlugin before_tool_callback \n", + "760 evaluation NaN get_all_tool_responses \n", "\n", - " ts_namespace ts_member_of ts_name type score \\\n", - "942 tools.mcp MCPToolset constructor constructor 0.5564 \n", - "762 telemetry NaN traceToolCall function 0.5551 \n", - "1280 plugins LoggingPlugin constructor constructor 0.5527 \n", - "1006 tools BaseToolset constructor constructor 0.5524 \n", - "943 tools GoogleSearchTool constructor constructor 0.5521 \n", + " typescript_namespace typescript_member_of typescript_name \\\n", + "110 tools GoogleSearchTool runAsync \n", + "1007 tools.mcp MCPToolset getTools \n", + "1008 tools.mcp MCPToolset close \n", + "209 plugins SecurityPlugin beforeToolCallback \n", + "760 events NaN getFunctionResponses \n", "\n", - " match confidence \n", - "942 True low \n", - "762 True low \n", - "1280 True low \n", - "1006 True low \n", - "943 True low " + " type score match confidence \n", + "110 method 0.554348 False high \n", + "1007 method 0.553750 False high \n", + "1008 method 0.553750 False high \n", + "209 method 0.553125 False high \n", + "760 function 0.552273 False high " ] }, - "execution_count": 12, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -680,7 +697,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "id": "f1e5b76d-22d2-432f-b3c6-5090b847cfe1", "metadata": {}, "outputs": [ @@ -721,42 +738,42 @@ " False\n", " high\n", " constructor\n", - " 377\n", + " 409\n", " \n", " \n", " function\n", - " 206\n", + " 268\n", " \n", " \n", " method\n", - " 604\n", + " 855\n", " \n", " \n", " True\n", " high\n", " constructor\n", - " 10\n", + " 1\n", " \n", " \n", " function\n", - " 8\n", + " 2\n", " \n", " \n", " method\n", - " 46\n", + " 24\n", " \n", " \n", " low\n", " constructor\n", - " 19\n", + " 8\n", " \n", " \n", " function\n", - " 45\n", + " 4\n", " \n", " \n", " method\n", - " 111\n", + " 7\n", " \n", " \n", "\n", @@ -765,18 +782,18 @@ "text/plain": [ " score\n", "match confidence type \n", - "False high constructor 377\n", - " function 206\n", - " method 604\n", - "True high constructor 10\n", - " function 8\n", - " method 46\n", - " low constructor 19\n", - " function 45\n", - " method 111" + "False high constructor 409\n", + " function 268\n", + " method 855\n", + "True high constructor 1\n", + " function 2\n", + " method 24\n", + " low constructor 8\n", + " function 4\n", + " method 7" ] }, - "execution_count": 13, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } diff --git a/proto/features.proto b/proto/features.proto index cf78926..e7390e6 100644 --- a/proto/features.proto +++ b/proto/features.proto @@ -71,5 +71,6 @@ message Feature { message FeatureRegistry { string language = 1; // ADK language (e.g., "GO"). string version = 2; // ADK version (e.g., "1.4.2"). - repeated Feature features = 3; + string commit_id = 3; // Last commit id of the repository. + repeated Feature features = 4; } diff --git a/proto2py.sh b/proto2py.sh index bab0bd7..4c975eb 100755 --- a/proto2py.sh +++ b/proto2py.sh @@ -1,2 +1,3 @@ + #!/bin/bash protoc -I=proto --python_out=src/google/adk/scope proto/features.proto diff --git a/pyproject.toml b/pyproject.toml index fe81589..8e1154e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dependencies = [ "scipy", "numpy", "jellyfish", - "RapidFuzz", "pandas", ] diff --git a/report.sh b/report.sh index 9d2192a..28b7570 100755 --- a/report.sh +++ b/report.sh @@ -4,7 +4,7 @@ set -e # Default values -REPORT_TYPE="md" + VERBOSE="" COMMON="" @@ -31,10 +31,7 @@ while [[ "$#" -gt 0 ]]; do OUTPUT_DIR="$2" shift 2 ;; - --report-type) - REPORT_TYPE="$2" - shift 2 - ;; + -v|--verbose) VERBOSE="--verbose" shift @@ -83,9 +80,10 @@ EXTENSION="md" # Standard 2-way report OUTPUT_FILENAME="${LANG_CODES[0]}_${LANG_CODES[1]}.${EXTENSION}" -# Ensure report type is 'md' for standard logic so unified generator runs -if [ "$REPORT_TYPE" == "raw" ]; then - REPORT_TYPE="md" + +# Check if we are running in matrix mode (CSV inputs) +if [[ "${REGISTRIES[0]}" == *.csv ]]; then + OUTPUT_FILENAME="matrix_report.md" fi FULL_OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_FILENAME}" @@ -100,6 +98,7 @@ export PYTHONPATH="${SCRIPT_DIR}/src:${PYTHONPATH}" python3 "${SCRIPT_DIR}/src/google/adk/scope/reporter/reporter.py" \ --registries "${REGISTRIES[@]}" \ --output "${FULL_OUTPUT_PATH}" \ - --report-type "${REPORT_TYPE}" \ + ${COMMON} \ ${VERBOSE} + diff --git a/run.sh b/run.sh index 4ca47f4..127dbd4 100755 --- a/run.sh +++ b/run.sh @@ -13,19 +13,19 @@ echo "Extracting Go features..." # Py -> TS echo "Generating raw and markdown reports..." -./report.sh --base output/py.txtpb --target output/ts.txtpb --output ./output --report-type md +./report.sh --base output/python.txtpb --target output/typescript.txtpb --output ./output # Py -> Java echo "Generating raw and markdown reports..." -./report.sh --base output/py.txtpb --target output/java.txtpb --output ./output --report-type md +./report.sh --base output/python.txtpb --target output/java.txtpb --output ./output # Py -> Go echo "Generating raw and markdown reports..." -./report.sh --base output/py.txtpb --target output/go.txtpb --output ./output --report-type md +./report.sh --base output/python.txtpb --target output/go.txtpb --output ./output # Matrix reports -#echo "Generating matrix reports..." -#./report.sh --registries output/py.txtpb output/ts.txtpb output/java.txtpb output/go.txtpb --output ./output --report-type matrix --common \ No newline at end of file +echo "Generating matrix reports..." +# ./report.sh --registries output/py_go.csv output/py_java.csv output/py_ts.csv --output ./output \ No newline at end of file diff --git a/src/google/adk/scope/extractors/converter_go.py b/src/google/adk/scope/extractors/converter_go.py index 4b2cd8c..21d7ee9 100644 --- a/src/google/adk/scope/extractors/converter_go.py +++ b/src/google/adk/scope/extractors/converter_go.py @@ -180,8 +180,7 @@ def _extract_docstring(self, node: Node) -> str: return "\n".join(comments) def _extract_interface_name(self, node: Node) -> str: - """Walk up the AST from a method_spec to find the interface type name. - """ + """Walk up the AST from a method_spec to find the interface type name.""" parent = node.parent while parent: if parent.type == "type_spec": diff --git a/src/google/adk/scope/extractors/extract.py b/src/google/adk/scope/extractors/extract.py index 543d2bb..b059dba 100644 --- a/src/google/adk/scope/extractors/extract.py +++ b/src/google/adk/scope/extractors/extract.py @@ -1,4 +1,5 @@ import logging +import subprocess import sys from pathlib import Path @@ -13,6 +14,7 @@ extractor_ts, ) from google.adk.scope.features_pb2 import FeatureRegistry +from google.adk.scope.utils import string from google.adk.scope.utils.args import parse_args logging.basicConfig( @@ -79,6 +81,21 @@ def get_search_dir(input_path: Path, language: str) -> Path: return input_path +def get_latest_commit_id(repo_path: Path) -> str: + """Gets the latest commit ID from a git repository.""" + try: + # Run 'git rev-parse HEAD' to get the full SHA + commit_id = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + cwd=str(repo_path), + text=True, + stderr=subprocess.DEVNULL, + ).strip() + return commit_id + except (subprocess.CalledProcessError, FileNotFoundError): + return "" + + def main(): args = ( parse_args() @@ -210,10 +227,13 @@ def main(): repo_root if repo_root else Path(".") ) + commit_id = get_latest_commit_id(repo_root if repo_root else Path(".")) + registry = FeatureRegistry( language=args.language.upper(), version=version, features=all_features, + commit_id=commit_id, ) output_dir = args.output @@ -223,15 +243,7 @@ def main(): logger.error("Failed to create output directory %s: %s", output_dir, e) sys.exit(1) - prefix = ( - "py" - if args.language in {"python", "py"} - else ( - "ts" - if args.language in {"typescript", "ts"} - else "java" if args.language == "java" else "go" - ) - ) + prefix = string.get_language_name(args.language).lower() base_filename = f"{prefix}" if _JSON_OUTPUT: diff --git a/src/google/adk/scope/features_pb2.py b/src/google/adk/scope/features_pb2.py index 48e9c3f..b79c043 100644 --- a/src/google/adk/scope/features_pb2.py +++ b/src/google/adk/scope/features_pb2.py @@ -24,15 +24,15 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x0fgoogle.adk.meta\"\xc4\x01\n\x05Param\x12\x15\n\roriginal_name\x18\x01 \x01(\t\x12\x17\n\x0fnormalized_name\x18\x02 \x01(\t\x12\x16\n\x0eoriginal_types\x18\x03 \x03(\t\x12\x34\n\x10normalized_types\x18\x04 \x03(\x0e\x32\x1a.google.adk.meta.ParamType\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x0bis_optional\x18\x06 \x01(\x08\x42\x0e\n\x0c_description\"\xdc\x04\n\x07\x46\x65\x61ture\x12\x15\n\roriginal_name\x18\x01 \x01(\t\x12\x17\n\x0fnormalized_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x11\n\tmember_of\x18\x04 \x01(\t\x12\x1c\n\x14normalized_member_of\x18\x05 \x01(\t\x12\x38\n\x08maturity\x18\x06 \x01(\x0e\x32!.google.adk.meta.Feature.MaturityH\x01\x88\x01\x01\x12+\n\x04type\x18\x07 \x01(\x0e\x32\x1d.google.adk.meta.Feature.Type\x12\x11\n\tfile_path\x18\x08 \x01(\t\x12\x11\n\tnamespace\x18\t \x01(\t\x12\x1c\n\x14normalized_namespace\x18\n \x01(\t\x12*\n\nparameters\x18\x0b \x03(\x0b\x32\x16.google.adk.meta.Param\x12\x1d\n\x15original_return_types\x18\x0c \x03(\t\x12\x1f\n\x17normalized_return_types\x18\r \x03(\t\x12\x12\n\x05\x61sync\x18\x0e \x01(\x08H\x02\x88\x01\x01\"6\n\x08Maturity\x12\x10\n\x0c\x45XPERIMENTAL\x10\x00\x12\x08\n\x04\x42\x45TA\x10\x01\x12\x0e\n\nDEPRECATED\x10\x02\"L\n\x04Type\x12\x0c\n\x08\x46UNCTION\x10\x00\x12\x13\n\x0fINSTANCE_METHOD\x10\x01\x12\x10\n\x0c\x43LASS_METHOD\x10\x02\x12\x0f\n\x0b\x43ONSTRUCTOR\x10\x03\x42\x0e\n\x0c_descriptionB\x0b\n\t_maturityB\x08\n\x06_async\"`\n\x0f\x46\x65\x61tureRegistry\x12\x10\n\x08language\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12*\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x18.google.adk.meta.Feature*o\n\tParamType\x12\n\n\x06OBJECT\x10\x00\x12\n\n\x06STRING\x10\x01\x12\n\n\x06NUMBER\x10\x02\x12\x0b\n\x07\x42OOLEAN\x10\x03\x12\x08\n\x04LIST\x10\x04\x12\x07\n\x03MAP\x10\x05\x12\x07\n\x03SET\x10\x06\x12\x0b\n\x07UNKNOWN\x10\x07\x12\x08\n\x04NULL\x10\x08\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x0fgoogle.adk.meta\"\xc4\x01\n\x05Param\x12\x15\n\roriginal_name\x18\x01 \x01(\t\x12\x17\n\x0fnormalized_name\x18\x02 \x01(\t\x12\x16\n\x0eoriginal_types\x18\x03 \x03(\t\x12\x34\n\x10normalized_types\x18\x04 \x03(\x0e\x32\x1a.google.adk.meta.ParamType\x12\x18\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x0bis_optional\x18\x06 \x01(\x08\x42\x0e\n\x0c_description\"\xdc\x04\n\x07\x46\x65\x61ture\x12\x15\n\roriginal_name\x18\x01 \x01(\t\x12\x17\n\x0fnormalized_name\x18\x02 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x11\n\tmember_of\x18\x04 \x01(\t\x12\x1c\n\x14normalized_member_of\x18\x05 \x01(\t\x12\x38\n\x08maturity\x18\x06 \x01(\x0e\x32!.google.adk.meta.Feature.MaturityH\x01\x88\x01\x01\x12+\n\x04type\x18\x07 \x01(\x0e\x32\x1d.google.adk.meta.Feature.Type\x12\x11\n\tfile_path\x18\x08 \x01(\t\x12\x11\n\tnamespace\x18\t \x01(\t\x12\x1c\n\x14normalized_namespace\x18\n \x01(\t\x12*\n\nparameters\x18\x0b \x03(\x0b\x32\x16.google.adk.meta.Param\x12\x1d\n\x15original_return_types\x18\x0c \x03(\t\x12\x1f\n\x17normalized_return_types\x18\r \x03(\t\x12\x12\n\x05\x61sync\x18\x0e \x01(\x08H\x02\x88\x01\x01\"6\n\x08Maturity\x12\x10\n\x0c\x45XPERIMENTAL\x10\x00\x12\x08\n\x04\x42\x45TA\x10\x01\x12\x0e\n\nDEPRECATED\x10\x02\"L\n\x04Type\x12\x0c\n\x08\x46UNCTION\x10\x00\x12\x13\n\x0fINSTANCE_METHOD\x10\x01\x12\x10\n\x0c\x43LASS_METHOD\x10\x02\x12\x0f\n\x0b\x43ONSTRUCTOR\x10\x03\x42\x0e\n\x0c_descriptionB\x0b\n\t_maturityB\x08\n\x06_async\"s\n\x0f\x46\x65\x61tureRegistry\x12\x10\n\x08language\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x11\n\tcommit_id\x18\x03 \x01(\t\x12*\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x18.google.adk.meta.Feature*o\n\tParamType\x12\n\n\x06OBJECT\x10\x00\x12\n\n\x06STRING\x10\x01\x12\n\n\x06NUMBER\x10\x02\x12\x0b\n\x07\x42OOLEAN\x10\x03\x12\x08\n\x04LIST\x10\x04\x12\x07\n\x03MAP\x10\x05\x12\x07\n\x03SET\x10\x06\x12\x0b\n\x07UNKNOWN\x10\x07\x12\x08\n\x04NULL\x10\x08\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'features_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_PARAMTYPE']._serialized_start=939 - _globals['_PARAMTYPE']._serialized_end=1050 + _globals['_PARAMTYPE']._serialized_start=958 + _globals['_PARAMTYPE']._serialized_end=1069 _globals['_PARAM']._serialized_start=36 _globals['_PARAM']._serialized_end=232 _globals['_FEATURE']._serialized_start=235 @@ -42,5 +42,5 @@ _globals['_FEATURE_TYPE']._serialized_start=724 _globals['_FEATURE_TYPE']._serialized_end=800 _globals['_FEATUREREGISTRY']._serialized_start=841 - _globals['_FEATUREREGISTRY']._serialized_end=937 + _globals['_FEATUREREGISTRY']._serialized_end=956 # @@protoc_insertion_point(module_scope) diff --git a/src/google/adk/scope/reporter/markdown.py b/src/google/adk/scope/reporter/markdown.py index 11f6cae..57e6359 100644 --- a/src/google/adk/scope/reporter/markdown.py +++ b/src/google/adk/scope/reporter/markdown.py @@ -5,6 +5,7 @@ import pandas as pd from google.adk.scope import features_pb2 +from google.adk.scope.utils import reporting, string @dataclasses.dataclass @@ -13,36 +14,6 @@ class MarkdownReport: module_reports: Dict[str, str] # filename -> content -def _get_language_code(language_name: str) -> str: - """Returns a short code for the language.""" - name = language_name.upper() - if name in {"PYTHON", "PY"}: - return "py" - elif name in {"TYPESCRIPT", "TS"}: - return "ts" - elif name == "JAVA": - return "java" - elif name in {"GOLANG", "GO"}: - return "go" - else: - return name.lower() - - -def _get_language_name(language_name: str) -> str: - """Returns a properly capitalized display name for the language.""" - name = language_name.upper() - if name in {"PYTHON", "PY"}: - return "Python" - elif name in {"TYPESCRIPT", "TS"}: - return "TypeScript" - elif name == "JAVA": - return "Java" - elif name in {"GOLANG", "GO"}: - return "Go" - else: - return language_name.title() - - class MarkdownReportGenerator: def __init__( self, @@ -54,10 +25,10 @@ def __init__( self.target_registry = target_registry self.df = df - self.base_code = _get_language_code(base_registry.language) - self.target_code = _get_language_code(target_registry.language) - self.base_name = _get_language_name(base_registry.language) - self.target_name = _get_language_name(target_registry.language) + self.base_name = string.get_language_name(base_registry.language) + self.target_name = string.get_language_name(target_registry.language) + self.base_code = self.base_name.lower() + self.target_code = self.target_name.lower() def generate(self) -> MarkdownReport: """Generates a Markdown parity report from the DataFrame.""" @@ -67,15 +38,17 @@ def generate(self) -> MarkdownReport: "# Feature Matching Parity Report", f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "", - "| Role | Language | Version |", - "| :--- | :--- | :--- |", + "| Role | Language | Version | Last Commit |", + "| :--- | :--- | :--- | :--- |", ( f"| **Base** | {self.base_registry.language} |" f" {self.base_registry.version} |" + f" {self.base_registry.commit_id or 'N/A'} |" ), ( f"| **Target** | {self.target_registry.language} |" f" {self.target_registry.version} |" + f" {self.target_registry.commit_id or 'N/A'} |" ), "", ] @@ -86,10 +59,9 @@ def generate(self) -> MarkdownReport: master_lines.append("") header = ( - f"| Module | Features ({self.base_name}) | Score | Status | " - f"Details |" + f"| Module | Features ({self.base_name}) | Overlap | " f"Details |" ) - divider = "|---|---|---|---|---|" + divider = "|---|---|---|---|" master_lines.extend(["## Module Summary", header, divider]) @@ -99,15 +71,26 @@ def generate(self) -> MarkdownReport: # Determine cols based on language codes col_ns = f"{self.base_code}_namespace" - # Group by base namespace + # Split DataFrame: Base Present vs Target Only + # Target Only rows have empty base_name (and score 0.0) + # Note: We check if base_name is empty/NaN. + # In clean_dataframe terms it might be "___", but here it is "" from raw.py + df_target_only = self.df[self.df[f"{self.base_code}_name"] == ""] + df_base_present = self.df[self.df[f"{self.base_code}_name"] != ""] + + # Group by base namespace (for Base Present) # If namespace is empty, group under "Unknown Module" - self.df["_module_group"] = self.df[col_ns].replace("", "Unknown Module") + # We need to act on a copy to avoid SettingWithCopyWarning + df_base_present = df_base_present.copy() + df_base_present["_module_group"] = df_base_present[col_ns].replace( + "", "Unknown Module" + ) - grouped = self.df.groupby("_module_group") + grouped = df_base_present.groupby("_module_group") total_high = 0 total_low = 0 - total_base_features = len(self.df) + total_base_features = len(df_base_present) for module, group in grouped: # Calculate module stats @@ -150,10 +133,9 @@ def generate(self) -> MarkdownReport: module_reports[module_filename] = module_content # Add summary row - status_icon = "✅" if score == 1.0 else "⚠️" if score > 0.5 else "❌" row_str = ( f"| `{module}` | {module_total} | " - f"{score:.2%} | {status_icon} | " + f"{score:.2%} | " f"[View Details]({{modules_dir}}/{module_filename}) |" ) module_rows.append((score, row_str)) @@ -181,18 +163,69 @@ def generate(self) -> MarkdownReport: f"Likely matches needing verification |\n" f"| **❌ Mismatches** | **{base_exclusive}** | " f"No suitable match found in `{self.target_name}` |\n" - f"| **📊 Coverage Score** | **{parity_score:.2%}** | " + f"| **📊 Coverage Overlap** | **{parity_score:.2%}** | " f"Matches / Total Base Features ({total_matches} / " f"{total_base_features}) |" ) master_lines[global_score_idx] = global_stats + # -- Target Exclusive Section -- + if not df_target_only.empty: + target_section = self._generate_target_exclusive_section( + df_target_only + ) + master_lines.append("") + master_lines.append(target_section) + return MarkdownReport( main_report_content="\n".join(master_lines).strip(), module_reports=module_reports, ) + def _generate_target_exclusive_section(self, df: pd.DataFrame) -> str: + """Generates a section in the request for Target-Only features.""" + lines = [ + "## Target-Exclusive Modules", + "", + f"Features found in **{self.target_name}** but NOT in **{self.base_name}**.", + "", + ] + + col_ns = f"{self.target_code}_namespace" + + # Determine modules + # Avoid SettingWithCopyWarning + df_copy = df.copy() + df_copy["_target_module"] = df_copy[col_ns].replace( + "", "Unknown Module" + ) + + # Group + grouped = df_copy.groupby("_target_module") + + # Table + lines.append(f"| Target Module | Exclusive Features | Details |") + lines.append("| :--- | :--- | :--- |") + + rows = [] + for module, group in grouped: + count = len(group) + + # List top 3 examples + examples = group[f"{self.target_code}_name"].head(3).tolist() + example_str = ", ".join([f"`{e}`" for e in examples]) + if count > 3: + example_str += ", ..." + + rows.append(f"| `{module}` | {count} | {example_str} |") + + # Sort rows by count desc or alpha? Let's sort by module name (default) + # Actually keys are already sorted by groupby default + + lines.extend(rows) + return "\n".join(lines) + def _generate_module_content( self, module: str, @@ -207,6 +240,10 @@ def _generate_module_content( total_matches = high_conf + low_conf coverage = total_matches / total_features if total_features > 0 else 0.0 + # Replace empty values for display + # Replace empty values for display + group = reporting.clean_dataframe(group) + summary_table = ( "## Summary\n\n" "| Feature Category | Count | Details |\n" @@ -217,7 +254,7 @@ def _generate_module_content( f"Likely matches needing verification |\n" f"| **❌ Mismatches** | **{mismatches}** | " f"No suitable match found in `{self.target_name}` |\n" - f"| **📊 Coverage Score** | **{coverage:.2%}** | " + f"| **📊 Coverage Overlap** | **{coverage:.2%}** | " f"Matches / Total Base Features ({total_matches} / " f"{total_features}) |\n" ) @@ -254,20 +291,14 @@ def _generate_module_content( t_mem = row[f"{self.target_code}_member_of"] t_name = row[f"{self.target_code}_name"] - if t_name == "" and t_mem == "" and t_ns == "": + if t_name == "___" and t_mem == "___" and t_ns == "___": t_name = "*(None)*" score = row["score"] match_val = row["match"] conf_val = row["confidence"] - if match_val == "true": - if conf_val == "high": - match_icon = "✅" - else: - match_icon = "⚠️" - else: - match_icon = "❌" + match_icon = reporting.get_match_icon(match_val, conf_val) conf_display = conf_val.title() if conf_display == "High": diff --git a/src/google/adk/scope/reporter/matrix.py b/src/google/adk/scope/reporter/matrix.py new file mode 100644 index 0000000..63fd8b1 --- /dev/null +++ b/src/google/adk/scope/reporter/matrix.py @@ -0,0 +1,195 @@ +import dataclasses +from datetime import datetime +from typing import Dict, List + +import pandas as pd + +from google.adk.scope import features_pb2 +from google.adk.scope.utils import reporting, string + + +@dataclasses.dataclass +class MatrixReport: + content: str + + +class MatrixReportGenerator: + def __init__( + self, + match_dataframes: Dict[str, pd.DataFrame], + base_registry: features_pb2.FeatureRegistry = None, + target_registries: List[features_pb2.FeatureRegistry] = None, + base_language: str = None, + base_version: str = None, + ): + # Clean dataframes: replace NaN and empty strings with "___" + # We process copies to avoid side effects + cleaned_dfs = {} + for k, df in match_dataframes.items(): + # Clean dataframe + df_clean = reporting.clean_dataframe(df) + cleaned_dfs[k] = df_clean + + self.match_dataframes = cleaned_dfs + self.base_registry = base_registry + self.target_registries = target_registries or [] + + if base_registry: + self.base_name = string.get_language_name(base_registry.language) + self.base_code = self.base_name.lower() + self.base_version = base_registry.version + else: + self.base_name = string.get_language_name( + base_language or "Unknown" + ) + self.base_code = self.base_name.lower() + self.base_version = base_version or "Unknown" + + def generate(self) -> MatrixReport: + """Generates a Markdown matrix report.""" + lines = [] + lines.extend( + [ + "# Feature Matrix Report", + f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "", + f"**Base Language**: {self.base_name} " + f"({self.base_version})", + "", + ] + ) + + if not self.match_dataframes: + return MatrixReport("No data available.") + + # Use the first dataframe to get the base feature list + first_target_code = list(self.match_dataframes.keys())[0] + base_df = self.match_dataframes[first_target_code].copy() + + # Filter base dataframe to exclude empty base keys (target-only) + # This is critical for matrix report to avoid explosion + # Note: `clean_dataframe` replaces NaN and "" with "___" + col_name = f"{self.base_code}_name" + base_df = base_df[ + (base_df[col_name] != "") & (base_df[col_name] != "___") + ] + + # We only really need the base columns from this one + base_cols = [ + f"{self.base_code}_namespace", + f"{self.base_code}_member_of", + f"{self.base_code}_name", + ] + + # Initialize the consolidated dataframe with base columns + matrix_df = base_df[base_cols].copy() + + target_cols_info = [] + + # We iterate over match_dataframes keys if registries are not provided + # or iterate based on preferred order if registries ARE provided. + if self.target_registries: + target_codes = [ + string.get_language_name(r.language).lower() + for r in self.target_registries + ] + # Ensure we only use those that are in dataframes + target_iterator = [ + (code, string.get_language_name(code)) + for code in target_codes + if code in self.match_dataframes + ] + else: + # Sort keys for consistent output + target_codes = sorted(list(self.match_dataframes.keys())) + target_iterator = [ + (code, string.get_language_name(code)) for code in target_codes + ] + + for target_code, target_name in target_iterator: + + df = self.match_dataframes[target_code] + + def get_icon(row): + match = row.get("match", "false") + conf = row.get("confidence", "low") + return reporting.get_match_icon(match, conf) + + # Better approach: Just build the `icon` column in the source DF + # and rename it. + # Filter out target-only rows (where base_name is empty or placeholder) before processing + # to avoid merging on empty keys which causes explosion + # Note: `clean_dataframe` replaces NaN and "" with "___" + col_name = f"{self.base_code}_name" + # Explicitly verify column exists to avoid KeyError if base language was guessed wrong + if col_name not in df.columns: + # Try to fallback or skip? + # If base code is wrong, everything is broken. + pass + + df = df[(df[col_name] != "") & (df[col_name] != "___")].copy() + + df[f"status_{target_code}"] = df.apply(get_icon, axis=1) + + # We only need the status column to merge + matrix_df = pd.merge( + matrix_df, + df[[*base_cols, f"status_{target_code}"]], + on=base_cols, + how="left", + ) + + target_cols_info.append( + { + "code": target_code, + "name": target_name, + "col": f"status_{target_code}", + } + ) + + # Now we have `matrix_df` with base cols + status_{target} cols. + # Let's group by namespace (Module). + + col_ns = f"{self.base_code}_namespace" + matrix_df["_module_group"] = matrix_df[col_ns].replace( + ["", "___"], "Unknown Module" + ) + grouped = matrix_df.groupby("_module_group") + + # Table Header + # | Module | Container | Name | Java | Go | TS | + target_headers = " | ".join([f"{t['name']}" for t in target_cols_info]) + header = ( + f"| Module ({self.base_name}) | Container | Name | " + f"{target_headers} |" + ) + # Dynamic divider + target_dividers = " | ".join(["---"] * len(target_cols_info)) + divider = f"| :--- | :--- | :--- | {target_dividers} |" + + lines.extend([header, divider]) + + for module, group in grouped: + # Sort by name + group_sorted = group.sort_values( + by=[f"{self.base_code}_name"], ascending=[True] + ) + + for _, row in group_sorted.iterrows(): + ns = row[f"{self.base_code}_namespace"] + mem = row[f"{self.base_code}_member_of"] + name = row[f"{self.base_code}_name"] + + # Build row string + row_parts = [f"`{ns}`", f"`{mem}`", f"`{name}`"] + + for t in target_cols_info: + status = row[t["col"]] + # Handle NaN if merge failed (shouldn't happen) + if pd.isna(status): + status = "❓" + row_parts.append(status) + + lines.append(f"| {' | '.join(row_parts)} |") + + return MatrixReport(content="\n".join(lines)) diff --git a/src/google/adk/scope/reporter/raw.py b/src/google/adk/scope/reporter/raw.py index a32b47d..03b374f 100644 --- a/src/google/adk/scope/reporter/raw.py +++ b/src/google/adk/scope/reporter/raw.py @@ -4,6 +4,7 @@ import pandas as pd from google.adk.scope import features_pb2 +from google.adk.scope.utils import string from google.adk.scope.utils.similarity import SimilarityScorer # Global thresholds for match confidence @@ -29,20 +30,6 @@ def get_type_display_name(f: features_pb2.Feature) -> str: return "unknown" -def _get_lang_code(language: str) -> str: - """Returns a short code for the language (e.g. PYTHON -> py).""" - name = language.upper() - if name in {"PYTHON", "PY"}: - return "py" - elif name in {"TYPESCRIPT", "TS"}: - return "ts" - elif name == "JAVA": - return "java" - elif name in {"GOLANG", "GO"}: - return "go" - return name.lower() - - class RawReportGenerator: def __init__( self, @@ -54,8 +41,12 @@ def __init__( self.scorer = SimilarityScorer() # Pre-compute useful attributes - self.base_code = _get_lang_code(self.base_registry.language) - self.target_code = _get_lang_code(self.target_registry.language) + self.base_name = string.get_language_name(self.base_registry.language) + self.target_name = string.get_language_name( + self.target_registry.language + ) + self.base_code = self.base_name.lower() + self.target_code = self.target_name.lower() self.thresholds = SIMILARITY_THRESHOLDS.get( frozenset([self.base_code, self.target_code]), DEFAULT_THRESHOLDS, @@ -69,11 +60,21 @@ def __init__( def generate(self, output_path: Optional[str] = None) -> pd.DataFrame: """Generates the raw report DataFrame and optionally saves it to CSV.""" rows = [] + matched_target_ids = set() + for f_base in self.base_registry.features: best_match, best_score = self._find_best_match(f_base) + if best_match: + matched_target_ids.add(id(best_match)) row = self._create_row_data(f_base, best_match, best_score) rows.append(row) + # Process Target-Only Features + for f_target in self.target_registry.features: + if id(f_target) not in matched_target_ids: + row = self._create_row_data(None, f_target, 0.0) + rows.append(row) + df = self._create_dataframe(rows) if output_path: @@ -102,7 +103,7 @@ def _find_best_match( def _create_row_data( self, - f_base: features_pb2.Feature, + f_base: Optional[features_pb2.Feature], f_target: Optional[features_pb2.Feature], score: float, ) -> Dict[str, Any]: @@ -110,7 +111,10 @@ def _create_row_data( row: Dict[str, Any] = {} # Base columns - self._fill_feature_cols(row, f_base, self.base_code) + if f_base: + self._fill_feature_cols(row, f_base, self.base_code) + else: + self._fill_empty_cols(row, self.base_code) # Target columns if f_target: @@ -119,7 +123,12 @@ def _create_row_data( self._fill_empty_cols(row, self.target_code) # Metadata - row["type"] = get_type_display_name(f_base) + ref_feature = f_base if f_base else f_target + if ref_feature: + row["type"] = get_type_display_name(ref_feature) + else: + row["type"] = "unknown" + row["score"] = score # Match status diff --git a/src/google/adk/scope/reporter/reporter.py b/src/google/adk/scope/reporter/reporter.py index b89864d..4b35048 100644 --- a/src/google/adk/scope/reporter/reporter.py +++ b/src/google/adk/scope/reporter/reporter.py @@ -4,10 +4,11 @@ from pathlib import Path from typing import List, Optional +import pandas as pd from google.protobuf import text_format from google.adk.scope import features_pb2 -from google.adk.scope.reporter import markdown, raw +from google.adk.scope.reporter import markdown, matrix, raw from google.adk.scope.utils import args as adk_args @@ -19,12 +20,84 @@ def _read_feature_registry(file_path: str) -> features_pb2.FeatureRegistry: return registry +def generate_matrix_report_from_csvs( + csv_paths: List[str], + output_path: Path, +) -> None: + """Generates a matrix report from a list of CSV files.""" + match_dataframes = {} + + # We need to infer base language and version from the first CSV + # or just assume something common. + # The columns in CSV are: {base}_namespace, {base}_member_of, {base}_name, ... + + base_lang = None + + for csv_path in csv_paths: + try: + df = pd.read_csv(csv_path) + # Infer language codes from columns + # Expected columns: {base}_namespace, ..., {target}_namespace, ... + + # Find the base columns (first 3 usually) + # Actually we can regex the columns ending in _namespace + + cols = df.columns + namespaces = [c for c in cols if c.endswith("_namespace")] + + if len(namespaces) < 2: + logging.warning( + f"Skipping {csv_path}: Could not detect base/target languages." + ) + continue + + # Assume first namespace col is base (or find specific one?) + # Usually base is first in raw report. + base_code = namespaces[0].split("_")[0] + target_code = namespaces[1].split("_")[0] + + if base_lang is None: + base_lang = base_code + elif base_lang != base_code: + logging.warning( + f"Skipping {csv_path}: Base language mismatch ({base_code} != {base_lang})" + ) + continue + + # Add to dataframes + match_dataframes[target_code] = df + + except Exception as e: + logging.error(f"Error reading CSV {csv_path}: {e}") + sys.exit(1) + + if not match_dataframes: + logging.error("No valid CSV data found for matrix report.") + sys.exit(1) + + # Generate matrix report + # We pass base_language code. Version is unknown. + report_gen = matrix.MatrixReportGenerator( + match_dataframes, base_language=base_lang, base_version="Unknown" + ) + report = report_gen.generate() + + try: + output_path.write_text(report.content) + logging.info( + f"Successfully wrote matrix report from CSVs to {output_path}" + ) + except Exception as e: + logging.error(f"Error writing matrix report to {output_path}: {e}") + sys.exit(1) + + def generate_markdown_raw_reports( registries: List[features_pb2.FeatureRegistry], - report_type: str = "md", # Kept for backward compatibility output_path: Optional[Path] = None, -) -> markdown.MarkdownReport: +): """Matches features and generates reports.""" + # New unified flow for standard reports generator = raw.RawReportGenerator(registries[0], registries[1]) @@ -79,12 +152,12 @@ def generate_markdown_raw_reports( if csv_path.exists(): logging.info(f"Successfully wrote raw match report to {csv_path}") - return result - except Exception as e: logging.error(f"Error writing report to {output_path}: {e}") sys.exit(1) + return result + def main(): parser = argparse.ArgumentParser( @@ -114,12 +187,6 @@ def main(): "saved with same stem." ), ) - parser.add_argument( - "--report-type", - choices=["md", "matrix"], - default="md", - help="Type of gap report. 'md' or 'matrix' now produce both.", - ) adk_args.add_verbose_argument(parser) args = parser.parse_args() adk_args.configure_logging(args) @@ -136,6 +203,38 @@ def main(): ) sys.exit(1) + if len(registry_paths) < 2: + logging.error("Must provide registries or CSV files to compare.") + sys.exit(1) + + # Infer output path if directory + output_path = Path(args.output) + is_matrix_input = registry_paths[0].endswith(".csv") + + # If output looks like a directory (no extension) or is an existing dir + if output_path.suffix == "" or output_path.is_dir(): + output_path.mkdir(parents=True, exist_ok=True) + filename = "matrix_report.md" if is_matrix_input else "report.md" + output_path = output_path / filename + else: + # Check if we are in matrix mode and the filename is _.md (generated by script when langs unknown) + if is_matrix_input and output_path.name == "_.md": + output_path = output_path.with_name("matrix_report.md") + + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if inputs are CSVs + if is_matrix_input: + # Verify all are CSVs + if not all(p.endswith(".csv") for p in registry_paths): + logging.error( + "All inputs must be CSV files for CSV matrix report." + ) + sys.exit(1) + + generate_matrix_report_from_csvs(registry_paths, output_path) + return + if len(registry_paths) < 2: logging.error("Must provide at least 2 registries to compare.") sys.exit(1) @@ -145,12 +244,7 @@ def main(): logging.error(f"Error reading feature registries: {e}") sys.exit(1) - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - - generate_markdown_raw_reports( - registries, args.report_type, output_path=output_path - ) + generate_markdown_raw_reports(registries, output_path=output_path) if __name__ == "__main__": diff --git a/src/google/adk/scope/utils/reporting.py b/src/google/adk/scope/utils/reporting.py new file mode 100644 index 0000000..73a8703 --- /dev/null +++ b/src/google/adk/scope/utils/reporting.py @@ -0,0 +1,27 @@ +""" +Reporting utilities for ADK Scope. +""" + +from typing import Tuple + +import pandas as pd + + +def get_match_icon(match: str, confidence: str) -> str: + """Returns an icon representing the match status.""" + is_match = str(match).lower() == "true" + if is_match: + return "✅" if confidence == "high" else "⚠️" + return "❌" + + +def clean_dataframe(df: pd.DataFrame) -> pd.DataFrame: + """Replaces NaN and empty strings with a placeholder.""" + # Create a copy to avoid side effects + df_clean = df.copy() + df_clean = df_clean.fillna("___") + df_clean = df_clean.replace("", "___") + # Replace stringified nan if they exist + df_clean = df_clean.replace("nan", "___") + df_clean = df_clean.replace("NaN", "___") + return df_clean diff --git a/src/google/adk/scope/utils/string.py b/src/google/adk/scope/utils/string.py new file mode 100644 index 0000000..038c6eb --- /dev/null +++ b/src/google/adk/scope/utils/string.py @@ -0,0 +1,18 @@ +""" +Language normalization utilities for ADK Scope. +""" + + +def get_language_name(language_name: str) -> str: + """Returns a properly capitalized display name for the language.""" + name = language_name.upper() + if name in {"PYTHON", "PY"}: + return "Python" + elif name in {"TYPESCRIPT", "TS"}: + return "TypeScript" + elif name == "JAVA": + return "Java" + elif name in {"GOLANG", "GO"}: + return "Go" + else: + return language_name.title() diff --git a/src/google/adk/temp/readme b/src/google/adk/temp/readme new file mode 100644 index 0000000..f4a233d --- /dev/null +++ b/src/google/adk/temp/readme @@ -0,0 +1,2 @@ +Any file or folder in this directory is temporary and can be deleted. +It will be used for quick experiments. diff --git a/test/adk/scope/reporter/test_matrix.py b/test/adk/scope/reporter/test_matrix.py new file mode 100644 index 0000000..cdeece7 --- /dev/null +++ b/test/adk/scope/reporter/test_matrix.py @@ -0,0 +1,125 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pandas as pd + +from google.adk.scope import features_pb2 +from google.adk.scope.reporter import matrix, reporter + + +class TestMatrixReport(unittest.TestCase): + def setUp(self): + self.base_registry = features_pb2.FeatureRegistry( + language="PYTHON", version="1.0.0" + ) + self.java_registry = features_pb2.FeatureRegistry( + language="JAVA", version="1.0.0" + ) + self.go_registry = features_pb2.FeatureRegistry( + language="GO", version="1.0.0" + ) + + # Add some dummy features + f1 = self.base_registry.features.add() + f1.namespace = "google.cloud" + f1.original_name = "Client" + f1.type = features_pb2.Feature.Type.CLASS_METHOD + + f2 = self.java_registry.features.add() + f2.namespace = "com.google.cloud" + f2.original_name = "Client" + f2.type = features_pb2.Feature.Type.CLASS_METHOD + + def test_matrix_generation_structure(self): + # Create dummy dataframes + df_java = pd.DataFrame( + { + "py_namespace": ["google.cloud"], + "py_member_of": [""], + "py_name": ["Client"], + "java_namespace": ["com.google.cloud"], + "java_member_of": [""], + "java_name": ["Client"], + "match": ["true"], + "confidence": ["high"], + } + ) + + match_dataframes = {"java": df_java} + + gen = matrix.MatrixReportGenerator( + match_dataframes, self.base_registry, [self.java_registry] + ) + report = gen.generate() + + self.assertIn("# Feature Matrix Report", report.content) + self.assertIn("**Base Language**: Python", report.content) + self.assertIn( + "| Module (Python) | Container | Name | Java |", report.content + ) + self.assertIn( + "| `google.cloud` | `___` | `Client` | ✅ |", report.content + ) + + def test_csv_integration(self): + # Create temporary CSV files + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + csv1 = temp_path / "py_java.csv" + csv2 = temp_path / "py_go.csv" + # Output to a directory to test automatic filename appending + # NOTE: generate_matrix_report_from_csvs expects a FILE path. + # The directory logic is in main(); here we test the generator logic directly. + output_file = temp_path / "matrix_report.md" + + # Use empty string for namespace to test replacement with ___ + df1 = pd.DataFrame( + { + "py_namespace": [""], + "py_member_of": ["c1"], + "py_name": ["n1"], + "java_namespace": [""], + "java_member_of": ["c1"], + "java_name": ["n1"], + "match": ["true"], + "confidence": ["high"], + } + ) + df2 = pd.DataFrame( + { + "py_namespace": [""], + "py_member_of": ["c1"], + "py_name": ["n1"], + "go_namespace": [""], + "go_member_of": ["c1"], + "go_name": ["n1"], + "match": ["true"], + "confidence": ["low"], + } + ) + + df1.to_csv(csv1, index=False) + df2.to_csv(csv2, index=False) + + reporter.generate_matrix_report_from_csvs( + [str(csv1), str(csv2)], output_file + ) + + self.assertTrue(output_file.exists()) + content = output_file.read_text() + self.assertIn("___", content) + self.assertIn("# Feature Matrix Report", content) + self.assertTrue( + "| Java | Go |" in content or "| Go | Java |" in content + ) + if "| Java | Go |" in content: + # Java is high, Go is low + self.assertIn("| ✅ | ⚠️ |", content) + else: + self.assertIn("| ⚠️ | ✅ |", content) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/adk/scope/reporter/test_reporter.py b/test/adk/scope/reporter/test_reporter.py index 287e73c..ff428ad 100644 --- a/test/adk/scope/reporter/test_reporter.py +++ b/test/adk/scope/reporter/test_reporter.py @@ -111,7 +111,6 @@ def test_match_registries(self): output_path = Path(temp_dir) / "report.md" result_md = reporter.generate_markdown_raw_reports( [base_registry, target_registry], - report_type="md", output_path=output_path, ) report_md = result_md.main_report_content @@ -119,6 +118,11 @@ def test_match_registries(self): # 1. Verify Master Report Structure self.assertIn("# Feature Matching Parity Report", report_md) self.assertIn("## Summary", report_md) + # Check for new column + self.assertIn( + "| Role | Language | Version | Last Commit |", report_md + ) + # Check for High/Low confidence summaries self.assertIn( "| **✅ High Confidence Matches** | **1** |", report_md @@ -129,7 +133,7 @@ def test_match_registries(self): # Check for module entry in master summary self.assertIn( - "| Module | Features (Python) | Score | Status | Details |", + "| Module | Features (Python) | Overlap | Details |", report_md, ) self.assertIn("| `google.adk.events` |", report_md)